pi-openai-codex-compat 0.0.14 → 0.0.16

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 CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.16 - 2026-09-23
6
+
7
+ ### Added
8
+
9
+ - Opt-in Responses Lite for GPT-6 Sol and Luna on Pi 0.87.1.
10
+
11
+ ## 0.0.15 - 2026-09-23
12
+
13
+ ### Fixed
14
+
15
+ - Preserve earlier message omissions and replacements during overflow-recovery
16
+ compaction. Recover only the latest attempt Pi omitted for recovery, keeping
17
+ its committed progress without reviving unrelated content. Stored native
18
+ responses no longer override later assistant-message replacements.
19
+ - Rebuild the compaction request's tool declarations when a tool's parameter
20
+ schema changes under an unchanged name and description, instead of reusing a
21
+ stale schema from the previous turn.
22
+
5
23
  ## 0.0.14 - 2026-09-22
6
24
 
7
25
  ### Changed
package/README.md CHANGED
@@ -220,9 +220,10 @@ Defaults:
220
220
  | `reasoningMode` | `standard`, `pro` | `standard` | Controls supported models' execution mode independently of Pi's reasoning-effort control. The default omits `reasoning.mode`; `pro` sends `reasoning.mode: "pro"`. |
221
221
 
222
222
  Responses Lite supports exactly `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`,
223
- and `gpt-6-astra`. Pro reasoning mode supports `gpt-5.6`, `gpt-5.6-*`, and
224
- exactly `gpt-6-astra`. Both controls remain opt-in; other GPT-6 model IDs are
225
- not included.
223
+ `gpt-6-astra`, `gpt-6-sol`, and `gpt-6-luna`. Pro reasoning mode supports
224
+ `gpt-5.6`, `gpt-5.6-*`, and exactly `gpt-6-astra`. GPT-6 Sol and Luna use
225
+ standard reasoning mode even when `pro` is configured. Both controls remain
226
+ opt-in. GPT-6 Sol and Luna require Pi 0.87.1's model catalog.
226
227
 
227
228
  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.
228
229
 
@@ -328,9 +329,21 @@ The extension handles native compaction for `openai-codex`. It follows the Codex
328
329
  1. Send normal Responses history followed by `{ "type": "compaction_trigger" }`.
329
330
  2. Validate the returned opaque `compaction` item.
330
331
  3. Retain approximately 64,000 tokens of recent user messages, plus every developer and
331
- system message outside that budget.
332
+ system message outside that budget. Repeated prompt updates (for example several
333
+ `/reload` runs or prompt-section changes) each remain in place as their own inline
334
+ developer item; they accumulate across checkpoints and are never dropped, merged, or
335
+ deduplicated, because each one is a distinct instruction the model already saw.
332
336
  4. Persist the opaque checkpoint in the Pi session and replay it on later requests.
333
337
 
338
+ Overflow and length recovery follow Pi 0.87's `context_edit` semantics. Every request
339
+ reads Pi's session projection, so earlier omissions and replacements (an elided tool
340
+ output, an omitted reply) apply exactly as they do for Pi's own adapters. Before an
341
+ overflow-recovery compaction, Pi omits the latest failed or truncated attempt and its tool
342
+ results; the checkpoint request projects only those entries as they were before that
343
+ omission, so a truncated response and its tool results survive as committed progress while
344
+ every unrelated earlier edit stays in force. A failed attempt contributes only the native
345
+ output committed before the overflowing subrequest.
346
+
334
347
  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.
335
348
 
336
349
  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.
@@ -576,6 +589,15 @@ mise run check
576
589
  npm run pack:dry
577
590
  ```
578
591
 
592
+ `mise run check` runs the linters, formatters, type checks, and the offline
593
+ test suite; `mise run test` runs only the offline suite. Both tasks bind
594
+ `PI_PACKAGE_DIR` to `node_modules/@earendil-works/pi-coding-agent`, so tests
595
+ that load Pi in-process read this repository's Pi 0.87.1 resources even when a
596
+ global `PI_PACKAGE_DIR` points at another installation. Ordinary `pi` launches
597
+ outside these tasks are unaffected. Offline tests also exercise
598
+ `scripts/release.ts` with every child process mocked: they never run Git, npm,
599
+ Mise, or provider calls.
600
+
579
601
  Run the credentialed Pi/Codex integration tests separately. They load the real
580
602
  extension into headless Pi sessions, use the real WebSocket service, and ask
581
603
  the model to report all prior history markers after each text and tool
@@ -586,13 +608,31 @@ mise run test:live:codex
586
608
  ```
587
609
 
588
610
  The task obtains the local Codex bearer token and runs the tests with
589
- `gpt-5.6-luna` at medium reasoning effort.
611
+ `gpt-5.6-luna`, `gpt-6-sol`, and `gpt-6-luna` at medium reasoning effort.
590
612
  It also packs the extension and loads that archive through the shipped Pi
591
- 0.87.0 CLI. Ordinary Responses and Responses Lite tests exercise tool calls,
613
+ 0.87.1 CLI. Ordinary Responses and Responses Lite tests exercise tool calls,
592
614
  prompt reload, native compaction, and persisted-session resume against Codex.
615
+ All three models use standard reasoning mode. SDK tests verify Responses Lite
616
+ WebSocket history, prewarming, continuation, and the built-in read tool.
617
+ CLI tests also execute the built-in read tool after resume.
593
618
  Test credentials stay in memory and child-process environments. Test sessions
594
619
  and configuration are isolated from the user's agent directory.
595
620
 
621
+ The packaged-CLI test selects its archive and executable as follows:
622
+
623
+ - Without `PI_CODEX_PACKAGE_ARCHIVE`, it packs the current worktree into a
624
+ temporary directory. With `PI_CODEX_PACKAGE_ARCHIVE`, it loads exactly that
625
+ archive: the value resolves against the working directory, must be an
626
+ existing regular file that `tar` can list, and an empty, missing, directory,
627
+ or invalid value fails the test instead of falling back to a fresh pack.
628
+ - `PI_CODEX_CLI_PATH` selects another Pi `cli.js`; the default is this
629
+ repository's Pi 0.87.1 dependency. The test asserts that the selected
630
+ executable reports version 0.87.1, the only Pi version the packaged CLI test
631
+ is run against. The supported range is `>=0.87.0 <0.88.0`.
632
+ - Each Pi child process receives `PI_PACKAGE_DIR` bound to the selected
633
+ executable's package directory, so the runtime under test reads its own
634
+ metadata.
635
+
596
636
  The public package entrypoint is `extensions/index.ts`; implementation modules remain under
597
637
  `extensions/openai-codex-compat/`. The focused Pi AI serializer copy lives under
598
638
  `extensions/openai-codex-compat/vendor/pi-ai/`. The custom Codex provider transport and stream
@@ -606,10 +646,74 @@ search, and compaction continuation.
606
646
  1. Run `npm run release -- X.Y.Z` from a clean, synchronized `main`.
607
647
  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.
608
648
  3. Inspect the result, then push atomically with `git push --atomic origin main vX.Y.Z`.
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.
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>`.
649
+ 4. A read-only GitHub Actions job validates and packs the package. After approval
650
+ in the tag-restricted `npm-publish` environment, a separate GitHub-owned job
651
+ verifies the signature and signed digest before attesting and staging that
652
+ exact archive through npm trusted publishing.
653
+ 5. A final job creates the immutable GitHub release from the verified archive,
654
+ its checksum, and the version's changelog section (`Unreleased` for prereleases).
655
+ 6. Approve the staged package on npmjs.com or with `npm stage approve <stage-id>`.
656
+
657
+ The release command runs these steps in order:
658
+
659
+ 1. Refuse to start unless the branch is `main`, `git status --porcelain` is
660
+ empty, `CHANGELOG.md` has a section for the version, `HEAD` equals
661
+ `origin/main` after a fetch, and tag `vX.Y.Z` does not exist. Nothing has
662
+ changed when one of these checks fails.
663
+ 2. Write the version into `package.json` and `package-lock.json` and stage
664
+ only those two files.
665
+ 3. Check out the staged index into a temporary directory, run
666
+ `npm ci --ignore-scripts` and `npm pack` there, validate the package
667
+ identity, file list, and manifest, then run `mise run test:live:codex` in
668
+ the repository with `PI_CODEX_PACKAGE_ARCHIVE` set to that archive. This is
669
+ the exact archive release gate: the live CLI tests load that archive and
670
+ nothing else. A live process that cannot start, a nonzero exit, or a failed
671
+ package validation stops the release here.
672
+ 4. Create the SSH-signed `release: vX.Y.Z` commit with the archive's SHA-256
673
+ as its `Npm-Artifact-SHA256` trailer and verify the signature and trailer.
674
+ 5. Rebuild the package from the committed tree without repeating the live
675
+ tests and require the same digest.
676
+ 6. Create the lightweight tag and verify that it points at the release commit.
677
+
678
+ ### Recovering from a failed release command
679
+
680
+ Do not run any blanket `git restore`, `git reset`, `git checkout --`, or
681
+ `git clean`. Inspect first, then undo only what the failed command produced.
682
+
683
+ If the command fails during the version update, version changes can remain
684
+ in the worktree. Once the update and `git add` succeed, package, live-test,
685
+ and signing failures leave those changes staged. No release commit or tag
686
+ was created by that attempt. Inspect the state:
687
+
688
+ ```sh
689
+ git status --short
690
+ git diff -- package.json package-lock.json
691
+ git diff --cached -- package.json package-lock.json
692
+ ```
693
+
694
+ Undo only this attempt's version edits in the worktree and index. Preserve
695
+ concurrent changes, including edits in those same files. Do not stage whole
696
+ files containing unrelated edits. Fix the cause and confirm that `main` is
697
+ clean and synchronized before rerunning the release command.
698
+
699
+ If the command fails after the release commit (steps 4 to 6), a local signed
700
+ `release: vX.Y.Z` commit exists on `main`, and a local tag may exist. Do not rerun
701
+ the release command: it would refuse the unsynchronized `HEAD`, and a second
702
+ run on top of the existing commit would produce a duplicate release commit.
703
+ Do not push the commit or tag until signature verification, the reproducibility
704
+ check, and tag verification are complete. Inspect the commit and the digest:
705
+
706
+ ```sh
707
+ git log -1 --format='%H%n%s%n%(trailers:key=Npm-Artifact-SHA256,valueonly)' HEAD
708
+ git diff origin/main..HEAD -- package.json package-lock.json
709
+ git tag --list 'vX.Y.Z'
710
+ ```
711
+
712
+ Removing the local release commit or tag changes local refs. Confirm that
713
+ they were never pushed, record their hashes and a recovery path, and obtain
714
+ explicit approval before changing them. Never replace a published tag.
715
+ Fix the underlying cause before starting a new release from clean,
716
+ synchronized `main`.
613
717
 
614
718
  Stable releases use `latest`; prereleases derive their npm dist-tag from the first prerelease identifier.
615
719
 
@@ -47,6 +47,8 @@ export type RuntimeScope = {
47
47
  export type RequestTemplate = {
48
48
  modelId: string;
49
49
  payload: JsonRecord;
50
+ /** Fingerprint of the raw tool definitions that produced `payload.tools`. */
51
+ toolFingerprint: string;
50
52
  grammarToolInputProperties: GrammarToolInputProperties;
51
53
  requestOptions: OpenAICodexResponsesOptions;
52
54
  };
@@ -32,6 +32,7 @@ import {
32
32
  resolveRequestTranscript,
33
33
  responsesCompatibility,
34
34
  searchCheckpoint,
35
+ toolDefinitionFingerprint,
35
36
  type CheckpointData,
36
37
  type CompactionDecision,
37
38
  type GrammarToolInputProperties,
@@ -871,6 +872,7 @@ export class CodexProviderRuntime {
871
872
  this.templates.set(runtimeSessionId, {
872
873
  modelId: model.id,
873
874
  payload: replacementValues ? {} : withoutConversationInput(body),
875
+ toolFingerprint: toolDefinitionFingerprint(getCurrentTools(context.messages)),
874
876
  grammarToolInputProperties,
875
877
  requestOptions: { ...requestOptions },
876
878
  });
@@ -1,10 +1,11 @@
1
1
  import { isBoolean, isString } from "./value-contracts.ts";
2
- import { randomUUID } from "node:crypto";
2
+ import { createHash, randomUUID } from "node:crypto";
3
3
  import {
4
4
  buildSessionContext,
5
5
  buildSessionProjection,
6
6
  convertToLlm,
7
7
  sessionEntryToContextMessages,
8
+ type ContextEditEntry,
8
9
  type SessionEntry,
9
10
  type ToolInfo,
10
11
  } from "@earendil-works/pi-coding-agent";
@@ -31,6 +32,7 @@ import {
31
32
  type JsonRecord,
32
33
  } from "./codex-protocol.ts";
33
34
  import { nativeCommittedPrefixBeforeOverflow, nativeResponseOverrides } from "./native-history.ts";
35
+ import { stableResponsesJson } from "./responses-replay.ts";
34
36
  import {
35
37
  CODEX_NAMESPACED_TOOL_NAMES,
36
38
  CODEX_TEXT_CONTENT_ITEM_TOOL_RESULT_NAMES,
@@ -181,9 +183,45 @@ export function activeResponsesTools(
181
183
  : undefined;
182
184
  }
183
185
 
186
+ /** The raw tool definition fields that both Pi AI's `Tool` and Pi's `ToolInfo` carry. */
187
+ export type ToolDeclarationSource = Pick<ToolInfo, "name" | "description" | "parameters">;
188
+
184
189
  /**
185
- * Whether a serialized Responses `tools` list declares exactly the active tools,
186
- * by name and description. Namespace members count as `namespace.member`.
190
+ * Fingerprint raw tool definitions by name, description, and parameter schema,
191
+ * independent of declaration order. Both the transcript tools a turn request
192
+ * declared and the registry's active `ToolInfo` list produce the same value for
193
+ * the same definitions, so a cached turn template can be compared with the
194
+ * current tools without reversing the strict schema conversion applied on the
195
+ * wire.
196
+ */
197
+ export function toolDefinitionFingerprint(tools: readonly ToolDeclarationSource[]): string {
198
+ const declarations = tools
199
+ .map((tool) => {
200
+ // The JSON round-trip drops typebox symbol keys and undefined fields, as
201
+ // Pi AI's own declaration comparison does.
202
+ const parameters: unknown = JSON.parse(JSON.stringify(tool.parameters));
203
+ return { name: tool.name, description: tool.description, parameters };
204
+ })
205
+ .sort((left, right) => left.name.localeCompare(right.name));
206
+ return createHash("sha256").update(stableResponsesJson(declarations)).digest("hex");
207
+ }
208
+
209
+ /** Fingerprint of the registry tools that are currently active. */
210
+ export function activeToolFingerprint(
211
+ allTools: readonly ToolInfo[],
212
+ activeNames: readonly string[],
213
+ ): string {
214
+ const enabled = new Set(activeNames);
215
+ return toolDefinitionFingerprint(allTools.filter((tool) => enabled.has(tool.name)));
216
+ }
217
+
218
+ /**
219
+ * Whether a cached turn template still declares exactly the active tools.
220
+ *
221
+ * The serialized `tools` list must name the active set (namespace members
222
+ * count as `namespace.member`), and the raw definitions that produced it must
223
+ * fingerprint identically to the active registry tools. Comparing names and
224
+ * descriptions alone missed parameter-schema changes under an unchanged name.
187
225
  *
188
226
  * A cached turn template carries the exact declarations the turn requests sent,
189
227
  * including strict schema conversion that `ToolInfo` cannot reproduce. Reusing
@@ -191,9 +229,11 @@ export function activeResponsesTools(
191
229
  */
192
230
  export function declaresActiveTools(
193
231
  serialized: readonly JsonRecord[],
232
+ cachedToolFingerprint: string,
194
233
  allTools: readonly ToolInfo[],
195
234
  activeNames: readonly string[],
196
235
  ): boolean {
236
+ if (cachedToolFingerprint !== activeToolFingerprint(allTools, activeNames)) return false;
197
237
  const declared = new Map<string, string | undefined>();
198
238
  for (const item of serialized) {
199
239
  if (item.type === "namespace" && isString(item.name) && Array.isArray(item.tools)) {
@@ -415,18 +455,69 @@ export function checkpointData(
415
455
  return checkpoint;
416
456
  }
417
457
 
458
+ /**
459
+ * The entries Pi 0.87 omits for overflow or length recovery: the latest
460
+ * assistant attempt and the tool results of that turn, each targeted by a
461
+ * `context_edit` omission appended after the attempt. `leafId` is the parent of
462
+ * the first such omission, so a projection built up to it reflects every edit
463
+ * that existed before Pi omitted the attempt.
464
+ *
465
+ * Pi only selects a recovery attempt while it is still projected, so an
466
+ * omission of the latest assistant found here is Pi's recovery omission and
467
+ * not an unrelated edit.
468
+ */
469
+ function recoveryOmissions(
470
+ branch: readonly SessionEntry[],
471
+ ): { entryIds: ReadonlySet<string>; leafId: string } | undefined {
472
+ const attemptIndex = branch.findLastIndex(
473
+ (entry) => entry.type === "message" && entry.message.role === "assistant",
474
+ );
475
+ const attempt = branch[attemptIndex];
476
+ if (attempt?.type !== "message" || attempt.message.role !== "assistant") return undefined;
477
+
478
+ const toolCallIds = new Set(
479
+ attempt.message.content.flatMap((block) => (block.type === "toolCall" ? [block.id] : [])),
480
+ );
481
+ const candidates = new Set([attempt.id]);
482
+ for (const entry of branch.slice(attemptIndex + 1)) {
483
+ if (entry.type !== "message") continue;
484
+ if (entry.message.role !== "toolResult" || !toolCallIds.has(entry.message.toolCallId)) break;
485
+ candidates.add(entry.id);
486
+ }
487
+
488
+ const latestEdits = new Map<string, ContextEditEntry>();
489
+ for (const entry of branch.slice(attemptIndex + 1)) {
490
+ if (entry.type === "context_edit" && candidates.has(entry.targetId)) {
491
+ latestEdits.set(entry.targetId, entry);
492
+ }
493
+ }
494
+ if (latestEdits.get(attempt.id)?.replacement !== null) return undefined;
495
+ const omissions = new Set(
496
+ [...latestEdits.values()].filter((entry) => entry.replacement === null),
497
+ );
498
+ const first = branch.find((entry) => entry.type === "context_edit" && omissions.has(entry));
499
+ if (first?.type !== "context_edit" || first.parentId === null) return undefined;
500
+ return {
501
+ entryIds: new Set([...omissions].map((entry) => entry.targetId)),
502
+ leafId: first.parentId,
503
+ };
504
+ }
505
+
418
506
  /**
419
507
  * Materialize provider history for the active branch. Checkpoint history
420
508
  * replaces everything before its entry; later branch entries form the tail.
421
509
  *
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.
510
+ * Every request reads the session projection, so `context_edit` omissions and
511
+ * replacements apply exactly as they do for Pi's own adapters. Recovery
512
+ * compaction differs only for the latest attempt: Pi 0.87 omits the failed or
513
+ * truncated assistant and its tool results through `context_edit` entries
514
+ * before it runs `session_before_compact`. Those entries are projected as they
515
+ * were before Pi's omission, so a truncated boundary stays as committed
516
+ * progress while every earlier omission or replacement remains in force. The
517
+ * serializer skips error and aborted assistants; a failed attempt contributes
518
+ * only the native prefix committed before the overflowing subrequest. Entries
519
+ * are never removed from the branch copy because the omission entry's parent
520
+ * chain would break and the history would vanish.
430
521
  */
431
522
  export function providerHistory(options: {
432
523
  branch: readonly SessionEntry[];
@@ -459,13 +550,30 @@ export function providerHistory(options: {
459
550
  const projection = buildSessionProjection(branch);
460
551
  const projected = new Map<string, AgentMessages>();
461
552
  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) ?? []);
553
+ const recovery = options.recoverLatestOverflowPrefix ? recoveryOmissions(branch) : undefined;
554
+ if (recovery) {
555
+ for (const entry of buildSessionProjection(branch, recovery.leafId).entries) {
556
+ if (recovery.entryIds.has(entry.sourceEntry.id)) {
557
+ projected.set(entry.sourceEntry.id, entry.messages);
558
+ }
559
+ }
560
+ }
561
+ const entryMessages = (entry: SessionEntry): AgentMessages => projected.get(entry.id) ?? [];
466
562
 
467
563
  const checkpoint = searchCheckpoint(branch);
468
- const nativeAssistantItems = nativeResponseOverrides(branch, options.wireModel.id);
564
+ const nativeAssistantItems = new Map(nativeResponseOverrides(branch, options.wireModel.id));
565
+ for (const entry of branch) {
566
+ if (
567
+ entry.type === "message" &&
568
+ entry.message.role === "assistant" &&
569
+ entry.message.responseId &&
570
+ entryMessages(entry).some((message) => message !== entry.message)
571
+ ) {
572
+ // A replacement keeps the response ID but changes the model-visible content.
573
+ // Its original native output must not override the canonical replacement.
574
+ nativeAssistantItems.delete(entry.message.responseId);
575
+ }
576
+ }
469
577
  if (checkpoint.kind === "corrupt") {
470
578
  throw new Error("The latest Codex compaction checkpoint is corrupt.");
471
579
  }
@@ -258,15 +258,18 @@ export default function registerRemoteCompaction(
258
258
  });
259
259
  // Reuse the last turn's exact declarations when they still describe the
260
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.
261
+ // prompt-cache prefix. Rebuild from the registry when the active set or
262
+ // any definition (including a parameter schema under an unchanged name)
263
+ // changed since that turn, or when no turn template exists.
263
264
  const template: JsonRecord = { ...matching?.payload };
264
265
  const activeNames = pi.getActiveTools();
265
266
  const cachedTools = Array.isArray(matching?.payload.tools)
266
267
  ? matching.payload.tools.filter(isObject)
267
268
  : undefined;
268
269
  template.tools =
269
- cachedTools && declaresActiveTools(cachedTools, allTools, activeNames)
270
+ cachedTools &&
271
+ matching &&
272
+ declaresActiveTools(cachedTools, matching.toolFingerprint, allTools, activeNames)
270
273
  ? cachedTools
271
274
  : (activeResponsesTools(allTools, activeNames, grammarToolInputProperties) ?? []);
272
275
  const requestOptions: OpenAICodexResponsesOptions = {
@@ -10,6 +10,8 @@ const RESPONSES_LITE_MODELS: ReadonlySet<string> = new Set([
10
10
  "gpt-5.6-terra",
11
11
  "gpt-5.6-luna",
12
12
  "gpt-6-astra",
13
+ "gpt-6-sol",
14
+ "gpt-6-luna",
13
15
  ]);
14
16
 
15
17
  function isHostedTool(tool: JsonRecord): boolean {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-openai-codex-compat",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
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.87.0",
55
- "@earendil-works/pi-coding-agent": "0.87.0",
56
- "@earendil-works/pi-tui": "0.87.0",
54
+ "@earendil-works/pi-ai": "0.87.1",
55
+ "@earendil-works/pi-coding-agent": "0.87.1",
56
+ "@earendil-works/pi-tui": "0.87.1",
57
57
  "@types/node": "22.20.1",
58
58
  "oxfmt": "0.64.0",
59
59
  "oxlint": "1.79.0",