pi-openai-toolkit 0.10.0 → 0.11.1

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/NOTICE CHANGED
@@ -1,33 +1,55 @@
1
- pi-openai-toolkit
2
- Copyright (c) 2026 awoaCrim and contributors
3
-
4
- The native OpenAI Web Search behavior in this package was adapted from
5
- code-yeongyu/pi-openai-web-search at commit
6
- 39643380682f02f306b0de2673ff136c45ccc2a2.
7
-
8
- Upstream copyright and license notice:
9
-
10
- Copyright (c) 2026 Yeongyu Kim
11
-
12
- Permission is hereby granted, free of charge, to any person obtaining a copy
13
- of this software and associated documentation files (the "Software"), to deal
14
- in the Software without restriction, including without limitation the rights
15
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
- copies of the Software, and to permit persons to whom the Software is
17
- furnished to do so, subject to the following conditions:
18
-
19
- The above copyright notice and this permission notice shall be included in all
20
- copies or substantial portions of the Software.
21
-
22
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
- SOFTWARE.
29
-
30
- The adapted implementation targets the @earendil-works Pi interfaces and is
31
- integrated with pi-openai-toolkit's independent compaction and Web Search
32
- extension entry points. The upstream NOTICE also identifies the behavior as a
33
- standalone port of the senpi-mono built-in openai-web-search extension.
1
+ pi-openai-toolkit
2
+ Copyright (c) 2026 awoaCrim and contributors
3
+
4
+ The native OpenAI Web Search behavior in this package was adapted from
5
+ code-yeongyu/pi-openai-web-search at commit
6
+ 39643380682f02f306b0de2673ff136c45ccc2a2.
7
+
8
+ Upstream copyright and license notice:
9
+
10
+ Copyright (c) 2026 Yeongyu Kim
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+
30
+ The adapted implementation targets the @earendil-works Pi interfaces and is
31
+ integrated with pi-openai-toolkit's independent compaction and Web Search
32
+ extension entry points. The upstream NOTICE also identifies the behavior as a
33
+ standalone port of the senpi-mono built-in openai-web-search extension.
34
+
35
+ The stable reasoning-effort planner in src/codex-astra/effort-planner.ts was
36
+ ported from Oh My Pi (@oh-my-pi/pi-ai, providers/openai-configuration-update.ts)
37
+ at version 18.1.8.
38
+
39
+ Upstream copyright and license notice:
40
+
41
+ MIT License
42
+
43
+ Copyright (c) 2025 Mario Zechner
44
+ Copyright (c) 2025-2026 Can Bölük
45
+ Copyright (c) 2026 Stencil Labs, Inc.
46
+
47
+ Permission is hereby granted, free of charge, to any person obtaining a copy
48
+ of this software and associated documentation files (the "Software"), to deal
49
+ in the Software without restriction, including without limitation the rights
50
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
51
+ copies of the Software, and to permit persons to whom the Software is
52
+ furnished to do so, subject to the following conditions:
53
+
54
+ The above copyright notice and this permission notice shall be included in all
55
+ copies or substantial portions of the Software.
package/README.md CHANGED
@@ -10,12 +10,13 @@
10
10
 
11
11
  ---
12
12
 
13
- `pi-openai-toolkit` is an all-in-one OpenAI enhancement package for **Pi (>= 0.84.x)**. It bundles four extensions designed for OpenAI Responses endpoints:
13
+ `pi-openai-toolkit` is an all-in-one OpenAI enhancement package for **Pi (>= 0.84.x)**. It bundles five extensions designed for OpenAI Responses endpoints:
14
14
 
15
15
  1. **Remote Responses Compaction v2** (`extensions/compaction.ts`): Server-side opaque context compaction using the `remote_compaction_v2` SSE streaming protocol. Preserves context fidelity without lossy text re-summarization.
16
16
  2. **Model-Scoped Web Search** (`extensions/web-search.ts`): Toolkit-owned hosted Web Search for an exact `provider/model-id` allowlist configured with `webSearch.models`, across the supported `openai-responses` and `openai-codex-responses` APIs.
17
17
  3. **Native Image Generation** (`extensions/image-generation.ts`): A sequential local tool backed by the hosted Responses `image_generation` tool. It supports text-to-image and explicit local-reference edits while keeping binary image data out of session history.
18
18
  4. **Model-Reviewed Auto Mode** (`extensions/auto-mode.ts`): Pi does not prompt before running tools. Auto mode adds a gate on state-changing tool calls and lets a reviewer model you configure approve or deny each one, so the agent keeps working without interrupting you.
19
+ 5. **Codex GPT-6 Astra Compatibility** (`extensions/codex-astra.ts`): Sends the Codex backend version-gate header on live requests and keeps prompt caching alive across thinking-level changes on `gpt-6-astra` by carrying later effort changes as `configuration_update` transcript items.
19
20
 
20
21
  ## Installation
21
22
 
@@ -67,6 +68,11 @@ pi --no-extensions -e /absolute/path/to/pi-openai-toolkit/extensions/image-gener
67
68
  pi --no-extensions -e /absolute/path/to/pi-openai-toolkit/extensions/auto-mode.ts
68
69
  ```
69
70
 
71
+ ```bash
72
+ # Load only the Codex Astra compatibility layer
73
+ pi --no-extensions -e /absolute/path/to/pi-openai-toolkit/extensions/codex-astra.ts
74
+ ```
75
+
70
76
  > ⚠️ **Conflict Warning**: Do not run `pi-remote-compact`, `@lll9p/pi-better-compaction`, or standalone `pi-openai-web-search` alongside `pi-openai-toolkit`. Duplicate hooks cause race conditions and duplicated system prompts.
71
77
 
72
78
  ---
@@ -148,6 +154,12 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
148
154
  "recentDenials": 10,
149
155
  "windowSize": 50
150
156
  }
157
+ },
158
+ "codexAstra": {
159
+ "enabled": true,
160
+ "models": [
161
+ "openai-codex/gpt-6-astra"
162
+ ]
151
163
  }
152
164
  }
153
165
  ```
@@ -205,6 +217,11 @@ The request uses the current model's resolved Responses endpoint, authentication
205
217
 
206
218
  Auto mode is opt-in through the empty-by-default `models` allowlist because it authorizes a model to approve writes and shell commands on your behalf. Enable it for a session with `pi --auto` or `/auto on`, and use `/auto all` to review every tool at once. See [Core Mechanics](#core-mechanics) section 5 for the exact decision and fallback rules.
207
219
 
220
+ #### `codexAstra`
221
+
222
+ - `enabled` (*boolean*, default: `false`): Master switch for the GPT-6 Astra compatibility layer: the Codex backend version-gate header on live requests, and the stable-effort `configuration_update` rewrite described in section 6 of Core Mechanics.
223
+ - `models` (*string[]*, default: `[]`): Exact `provider/model-id` allowlist of models that accept the `configuration_update` input item. Only Astra-era models answer it correctly (today `["openai-codex/gpt-6-astra"]`, or your gateway's equivalent key such as `"uwoacrimson/gpt-6-astra"`); the rewrite is never applied to any other model, and the version header applies to all `openai-codex-responses` requests regardless of this list.
224
+
208
225
  ---
209
226
 
210
227
  ## Core Mechanics
@@ -289,6 +306,18 @@ Pi does not prompt before running `bash`, `write`, or `edit`; the only built-in
289
306
  - **Bounded audit trail**: Every decision is persisted as a custom session entry (`pi-openai-toolkit.auto-mode-decision.v1`) containing the tool name, call id, decision, bounded rationale, reviewer model, risk level, user authorization, investigation rounds used, and decision source (`reviewer`, `classifier`, `human`, `policy`, or `circuit-breaker`). Custom entries never enter LLM context, and tool arguments are not recorded.
290
307
  - **Cost boundary**: Each gated call costs at least one reviewer completion, and enabling `evidenceTools` lets that review expand into up to `maxEvidenceRounds` extra completions. The pre-scorer adds one small completion per gated tool result when enabled. Reviewer completions run with prompt caching disabled, and the reviewer can execute nothing beyond the read-only tools listed above.
291
308
 
309
+ ### 6. Codex GPT-6 Astra Compatibility
310
+
311
+ GPT-6 Astra is a Codex-backend (ChatGPT OAuth) model with two wire behaviors that a plain OpenAI client does not get for free. This layer covers both, adapted from Oh My Pi 18.1.8 (MIT).
312
+
313
+ - **Backend version gate**: The Codex backend admits or hides model SKUs by the `version` header, and `gpt-6-astra` requires at least `0.153.0`. The extension sets that pin on every live `openai-codex-responses` request via `before_provider_headers` (only when the host has not set one), and the package's own synthetic requests - remote compaction v2, native compact v1, hosted image generation - carry the same pin from the shared header builder. The pinned value tracks the upstream `CODEX_CLIENT_VERSION`; when OpenAI raises the gate, that constant is what changes.
314
+ - **Stable effort with `configuration_update`**: OpenAI prompt caching compares the request from the first byte, and the request-level `reasoning.effort` sits at the top - changing thinking level mid-conversation normally busts the whole cached prefix. Astra instead accepts a `configuration_update` input item that changes effort from that point onward while the request-level field stays put. This extension implements that for the active conversation: the first request pins the baseline effort, every later change is spliced into the transcript at the position it should take effect (before the user message it applies to, or after the latest tool result inside a tool loop), and the same item is replayed at that position on every following request. The request body keeps the baseline byte-stable, so the cache prefix survives level changes.
315
+ - **History-rewrite safety**: Each spliced item is anchored to a fingerprint of the transcript position it was inserted at. If a later request's history no longer matches - compaction, branch switch, `/clear`, anything rewrote the transcript - the baseline is discarded and the next request re-pins its own effort, rather than placing an update at a stale position.
316
+ - **Compaction isolation**: The compact endpoints reject histories containing `configuration_update` items. This is enforced in two places: the payload rewriter never splices items into a `compaction_trigger`-shaped request, and both compaction clients strip any such item defensively from replayed input, so an Astra session can still be compacted by this package.
317
+ - **Scope**: The rewrite applies only to models listed in `codexAstra.models` on either Responses wire family (Astra is the only SKU known to accept the item; anything else answers HTTP 400 on it - the allowlist is what encodes that knowledge for gateways that proxy it), only when the request actually carries a wire-level effort, and never to `off`/`none`. Everything runs inside a try/catch: any unexpected payload shape or planner failure leaves the request untouched. To avoid double-splicing when both this toolkit and a host that implements the same behavior (Oh My Pi itself) are active, enable this layer on only one side.
318
+
319
+ Catalog note: Pi's bundled `openai-codex` model list does not include `gpt-6-astra` yet. Register it in `~/.pi/agent/models.json` under the built-in provider (custom models merge into the built-in list): `api: "openai-codex-responses"`, `baseUrl: "https://chatgpt.com/backend-api"`, `contextWindow: 272000`, `maxTokens: 128000`, and a `thinkingLevelMap` exposing `low` through `max`. Pi's Codex OAuth login supplies the token; this package reads it through Pi's `ModelRegistry` and never touches the credential store directly.
320
+
292
321
  ---
293
322
 
294
323
  ## Development & Testing
package/README_zh.md CHANGED
@@ -10,12 +10,13 @@
10
10
 
11
11
  ---
12
12
 
13
- `pi-openai-toolkit` 是专为 **Pi (>= 0.84.x)** 打造的 OpenAI 增强工具包,集成了四项针对 OpenAI Responses 协议的扩展:
13
+ `pi-openai-toolkit` 是专为 **Pi (>= 0.84.x)** 打造的 OpenAI 增强工具包,集成了五项针对 OpenAI Responses 协议的扩展:
14
14
 
15
15
  1. **远端无损压缩 (Remote Compaction v2)** (`extensions/compaction.ts`): 基于 `remote_compaction_v2` SSE 流式协议实现的服务端密文无损会话压缩,彻底告别会话变长后丢失细节的文本二次摘要。
16
16
  2. **按模型启用的联网搜索** (`extensions/web-search.ts`): 通过 `webSearch.models` 精确匹配 `provider/model-id`,由 Toolkit 为符合条件的 `openai-responses` 与 `openai-codex-responses` 会话提供并拥有原生联网搜索。
17
17
  3. **原生图像生成** (`extensions/image-generation.ts`): 注册由 Responses 托管 `image_generation` 工具驱动的本地顺序工具,支持文生图和显式本地参考图编辑,同时避免把图像二进制写入会话历史。
18
18
  4. **模型审批 Auto 模式** (`extensions/auto-mode.ts`): Pi 默认不会为工具调用弹出审批确认。Auto 模式为改变状态的工具调用加上一道审批门,由你指定的审批模型判定放行或拦截,让 Agent 不必停下来等你确认也能继续推进。
19
+ 5. **Codex GPT-6 Astra 适配** (`extensions/codex-astra.ts`): 为实时请求补发 Codex 后端版本门禁头,并在 `gpt-6-astra` 上把思考档位变更改写为 `configuration_update` 会话项,换档位不再打断提示缓存。
19
20
 
20
21
  ## 安装使用
21
22
 
@@ -65,6 +66,9 @@ pi --no-extensions -e /本地绝对路径/pi-openai-toolkit/extensions/image-gen
65
66
 
66
67
  # 仅加载 Auto 模式
67
68
  pi --no-extensions -e /本地绝对路径/pi-openai-toolkit/extensions/auto-mode.ts
69
+
70
+ # 仅加载 Codex Astra 适配
71
+ pi --no-extensions -e /本地绝对路径/pi-openai-toolkit/extensions/codex-astra.ts
68
72
  ```
69
73
 
70
74
  > ⚠️ **冲突提示**:请勿同时启用 `pi-remote-compact`、`@lll9p/pi-better-compaction` 或独立的 `pi-openai-web-search`。多重钩子会产生竞态或注入重复的 System Prompt。
@@ -148,6 +152,12 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
148
152
  "recentDenials": 10,
149
153
  "windowSize": 50
150
154
  }
155
+ },
156
+ "codexAstra": {
157
+ "enabled": true,
158
+ "models": [
159
+ "openai-codex/gpt-6-astra"
160
+ ]
151
161
  }
152
162
  }
153
163
  ```
@@ -201,10 +211,15 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
201
211
  - `classifier.model` (*string | null*): 预评分模型,格式 `provider/model-id`,未设置时沿用 `reviewerModel`。预评分只能把调用退回阻塞审批;除了缓存的 `low` 快速通道之外它无权放行,也永不拒绝。
202
212
  - `classifier.timeoutMs` (*整数*, 默认 `15000`) 与 `classifier.maxLag` (*整数*, 默认 `2`,范围 `0`–`20`): 经过超过 `maxLag` 个工具调用、或用户又说了新话之后,分数作废并强制走阻塞审批。
203
213
  - `circuitBreaker.consecutiveDenials` (*整数*, 默认 `3`,`0` 关闭): 单轮内连续拒绝多少次就直接结束这一轮,而不是让主模型无限拉锯。
204
- - `circuitBreaker.recentDenials` (*整数*, 默认 `10`,`0` 关闭) 与 `circuitBreaker.windowSize` (*整数*, 默认 `50`,范围 `1`–`200`): 最近 N 次受审调用里拒绝多少次就结束这一轮。它拓的是没有连续出现但密度很高的拒绝。
214
+ - `circuitBreaker.recentDenials` (*整数*, 默认 `10`,`0` 关闭) 与 `circuitBreaker.windowSize` (*整数*, 默认 `50`,范围 `1`–`200`): 最近 N 次受审调用里拒绝多少次就结束这一轮。它抓的是没有连续出现但密度很高的拒绝。
205
215
 
206
216
  Auto 模式需要显式启用(`models` 默认为空),因为它相当于允许一个模型代你批准文件写入和命令执行。会话内可用 `pi --auto` 或 `/auto on` 开启,`/auto all` 一次审批全部工具。具体判定与降级规则见「核心工作原理解析」第 5 节。
207
217
 
218
+ #### `codexAstra`(Codex GPT-6 Astra 适配配置)
219
+
220
+ - `enabled` (*boolean*, 默认 `false`): Astra 适配层总开关:实时请求的版本门禁头,以及「核心工作原理解析」第 6 节描述的 stable-effort 改写。
221
+ - `models` (*string[]*, 默认 `[]`): 接受 `configuration_update` 会话项的精确 `provider/model-id` 白名单。只有 Astra 世代模型能正确响应它(目前 `["openai-codex/gpt-6-astra"]`,或网关上对应的键如 `"uwoacrimson/gpt-6-astra"`);其他模型绝不会被改写。版本门禁头则对所有 `openai-codex-responses` 实时请求生效,不受这个列表限制。
222
+
208
223
  ---
209
224
 
210
225
  ## 核心工作原理解析
@@ -289,6 +304,18 @@ Pi 本身在运行 `bash`、`write`、`edit` 前不会询问确认,内置的
289
304
  - **有界审计记录**:每次判定都以自定义会话条目(`pi-openai-toolkit.auto-mode-decision.v1`)落盘,包含工具名、调用 id、结论、有界理由、审批模型、风险等级、用户授权程度、取证轮数,以及判定来源(`reviewer`、`classifier`、`human`、`policy` 或 `circuit-breaker`)。自定义条目不进入 LLM 上下文,且不记录工具参数。
290
305
  - **费用边界**:每个受控调用至少产生一次审批补全;开启 `evidenceTools` 后,这一次审批最多可膨胀为 `maxEvidenceRounds` 轮额外补全。开启预评分时,每个受控工具结果多一次小补全。审批请求禁用提示缓存,除上述只读工具外不能执行任何操作。
291
306
 
307
+ ### 6. Codex GPT-6 Astra 适配
308
+
309
+ GPT-6 Astra 是 Codex 后端(ChatGPT OAuth)上的模型,有两个普通 OpenAI 客户端不会自动处理的线协议行为。本适配层全部覆盖,代码改写自 Oh My Pi 18.1.8(MIT)。
310
+
311
+ - **后端版本门禁**:Codex 后端按 `version` 头决定放行或隐藏模型 SKU,`gpt-6-astra` 要求至少 `0.153.0`。扩展通过 `before_provider_headers` 给每条实时 `openai-codex-responses` 请求补发这个固定值(host 已设置时不覆盖);本包自己的合成请求(远端 v2、原生 v1、托管图像)由共享头构造器携带同一固定值。这个常量跟随上游 `CODEX_CLIENT_VERSION`:OpenAI 提高门禁时,改的就是它。
312
+ - **stable effort + `configuration_update`**:OpenAI 提示缓存从请求第一个字节开始比对,而请求级 `reasoning.effort` 就在头部——中途换思考档位通常会打爆整个缓存前缀。Astra 接受一种 `configuration_update` 输入项,从该项位置起改档位而不动请求级字段。本扩展把这套机制接到当前会话:首个请求钉住基线档位,之后的每次变更插入到它应生效的会话位置(作效于哪条用户消息就插在哪条之前,工具循环中途变更插在最近一条工具结果之后),后续请求在同一位置重放该项,请求体开头始终字节不变,缓存前缀存活。
313
+ - **历史重写安全**:每个插入项都锚定在它所在位置前一条内容的指纹上。后续请求的历史对不上(压缩、切分支、`/clear`,任何历史重写),基线直接作废,下一次请求用自己的档位重新钉基线,而不是把更新插到过期位置。
314
+ - **压缩隔离**:compact 端点拒绝含 `configuration_update` 的历史。两层防御:payload 改写器对带 `compaction_trigger` 的请求绝不插入;两个压缩客户端对回放输入里的该类项一律防御性剥离——所以 Astra 会话照常被本包压缩。
315
+ - **适用范围**:改写只作用于两种 Responses 线协议家族内 `codexAstra.models` 白名单中的模型(目前已知只有 astra 接受该 item,其他模型碰它就 400——网关代理同一模型时,白名单就是声明这件事的地方),且只改真正携带线上级 effort 的请求,`off`/`none` 不动。整个钩子在 try/catch 里,任何意外形状或规划失败都原样放行请求。若 host 自身已实现同样行为(例如 Oh My Pi),两边只开一个,避免双重插入。
316
+
317
+ 目录说明:Pi 内置的 `openai-codex` 模型列表还没有 `gpt-6-astra`,需要在 `~/.pi/agent/models.json` 内置 provider 下注册(自定义 models 会与内置列表合并):`api: "openai-codex-responses"`、`baseUrl: "https://chatgpt.com/backend-api"`、`contextWindow: 272000`、`maxTokens: 128000`,并用 `thinkingLevelMap` 声明 `low` 到 `max`。登录用 Pi 的 Codex OAuth 流程;本包通过 Pi 的 `ModelRegistry` 读凭据,不碰存储文件。
318
+
292
319
  ---
293
320
 
294
321
  ## 测试与验证
@@ -0,0 +1 @@
1
+ export { default } from "../src/codex-astra/extension.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-openai-toolkit",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "OpenAI-focused Pi toolkit providing remote Responses compaction, native Web Search, native image generation, and model-reviewed auto mode.",
@@ -34,7 +34,8 @@
34
34
  "./extensions/compaction.ts",
35
35
  "./extensions/web-search.ts",
36
36
  "./extensions/image-generation.ts",
37
- "./extensions/auto-mode.ts"
37
+ "./extensions/auto-mode.ts",
38
+ "./extensions/codex-astra.ts"
38
39
  ]
39
40
  },
40
41
  "files": [
@@ -65,6 +66,9 @@
65
66
  "src/auto-mode/reviewer.ts",
66
67
  "src/auto-mode/classifier.ts",
67
68
  "src/auto-mode/types.ts",
69
+ "extensions/codex-astra.ts",
70
+ "src/codex-astra/effort-planner.ts",
71
+ "src/codex-astra/extension.ts",
68
72
  "src/model-scope.ts",
69
73
  "src/native-fallback.ts",
70
74
  "src/payload-rewrite.ts",
@@ -0,0 +1,175 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ /**
4
+ * Stable request-level reasoning effort via `configuration_update` input items
5
+ * (GPT-6 Astra), ported from Oh My Pi (`@oh-my-pi/pi-ai`, MIT licensed,
6
+ * Copyright (c) 2025-2026 Can Bölük / Stencil Labs) to an extension-side
7
+ * payload rewrite.
8
+ *
9
+ * The request-level `reasoning.effort` is pinned to the value of the session's
10
+ * first request so the cached prompt prefix survives an effort change. Each
11
+ * later change is carried as a `configuration_update` item inserted at the
12
+ * tail of the transcript — before the user message it takes effect on, or
13
+ * after the latest tool result when the level changes inside a tool loop — and
14
+ * replayed at that position on every subsequent request until another update
15
+ * overrides it.
16
+ *
17
+ * Wire constraints (verified upstream against the Codex backend): only
18
+ * `gpt-6-astra` accepts the item type, consecutive updates are rejected, and
19
+ * `/responses/compact` rejects histories containing them — compaction
20
+ * requests are built outside this planner and never carry the items.
21
+ */
22
+
23
+ /** `configuration_update` input item; only `reasoning.effort` is updatable. */
24
+ export type ConfigurationUpdateItem = {
25
+ type: "configuration_update";
26
+ reasoning: { effort: string };
27
+ };
28
+
29
+ export type PlannableInputItem = {
30
+ type?: string | null;
31
+ role?: string;
32
+ id?: string | null;
33
+ status?: string | null;
34
+ [key: string]: unknown;
35
+ };
36
+
37
+ type EffortTransition = {
38
+ /** Input-array position the item is spliced into (before `input[index]`). */
39
+ index: number;
40
+ /** Fingerprint of `input[index - 1]` at record time; a mismatch means the history was rewritten. */
41
+ anchor: string;
42
+ effort: string;
43
+ };
44
+
45
+ /** Per-conversation effort baseline and recorded transitions. */
46
+ export type EffortControlState = {
47
+ baseEffort?: string;
48
+ currentEffort?: string;
49
+ transitions: EffortTransition[];
50
+ };
51
+
52
+ export type EffortPlanResult = {
53
+ /** The request-level effort that must accompany the returned input. */
54
+ effort: string;
55
+ /** Number of `configuration_update` items spliced into this request. */
56
+ spliced: number;
57
+ };
58
+
59
+ export function createEffortControlState(): EffortControlState {
60
+ return { transitions: [] };
61
+ }
62
+
63
+ const MAX_EFFORT_CONTROL_STATES = 16;
64
+
65
+ /**
66
+ * Fetch (or create) the control state for one conversation from a bounded
67
+ * LRU map, refreshing its slot.
68
+ */
69
+ export function getEffortControlState(states: Map<string, EffortControlState>, key: string): EffortControlState {
70
+ const existing = states.get(key);
71
+ if (existing) {
72
+ states.delete(key);
73
+ states.set(key, existing);
74
+ return existing;
75
+ }
76
+ const created = createEffortControlState();
77
+ states.set(key, created);
78
+ if (states.size > MAX_EFFORT_CONTROL_STATES) {
79
+ const oldest = states.keys().next().value;
80
+ if (oldest !== undefined) states.delete(oldest);
81
+ }
82
+ return created;
83
+ }
84
+
85
+ /**
86
+ * Fingerprint of the item a transition sits after. Output-only lifecycle
87
+ * fields are excluded: a live response item carries `id`/`status` that the
88
+ * sanitized replay of the same item drops.
89
+ */
90
+ function transitionAnchor(input: readonly PlannableInputItem[], index: number): string {
91
+ if (index === 0) return "";
92
+ const item = input[index - 1];
93
+ if (!item) return "";
94
+ const { id: _id, status: _status, ...stable } = item;
95
+ return createHash("sha1").update(JSON.stringify(stable)).digest("hex");
96
+ }
97
+
98
+ function resetEffortControlState(state: EffortControlState): void {
99
+ state.baseEffort = undefined;
100
+ state.currentEffort = undefined;
101
+ state.transitions = [];
102
+ }
103
+
104
+ /**
105
+ * Discard the baseline when the request no longer continues the conversation
106
+ * it was captured for: a wire history that shrank or was rewritten under a
107
+ * recorded transition (compaction, branch switch, `/clear`). The next request
108
+ * re-baselines from its own effort, which is what the API asks for after
109
+ * compaction anyway.
110
+ */
111
+ function syncEffortControlState(state: EffortControlState, input: readonly PlannableInputItem[]): void {
112
+ for (const transition of state.transitions) {
113
+ if (transition.index > input.length || transition.anchor !== transitionAnchor(input, transition.index)) {
114
+ resetEffortControlState(state);
115
+ return;
116
+ }
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Pin the request-level effort to the session baseline and splice pending
122
+ * `configuration_update` items into a copy of `input`.
123
+ *
124
+ * `input` is the freshly built transcript for this request, without any
125
+ * `configuration_update` items. `requested` is the wire effort the caller
126
+ * would otherwise send at the request level.
127
+ */
128
+ export function planStableEffort(
129
+ state: EffortControlState,
130
+ input: readonly PlannableInputItem[],
131
+ requested: string,
132
+ ): { input: Array<PlannableInputItem | ConfigurationUpdateItem>; result: EffortPlanResult } {
133
+ const next: Array<PlannableInputItem | ConfigurationUpdateItem> = [...input];
134
+ syncEffortControlState(state, next);
135
+ if (state.baseEffort === undefined) {
136
+ state.baseEffort = requested;
137
+ state.currentEffort = requested;
138
+ return { input: next, result: { effort: requested, spliced: 0 } };
139
+ }
140
+ if (state.currentEffort !== requested) {
141
+ const last = next[next.length - 1];
142
+ const index = last && last.role === "user" ? next.length - 1 : next.length;
143
+ const existing = state.transitions.find((transition) => transition.index === index);
144
+ if (existing) {
145
+ existing.effort = requested;
146
+ } else {
147
+ state.transitions.push({ index, anchor: transitionAnchor(next, index), effort: requested });
148
+ }
149
+ // A change back to the effort already in force at that position is a
150
+ // no-op on the wire; drop it rather than send a redundant item.
151
+ let preceding = state.baseEffort;
152
+ let precedingIndex = -1;
153
+ for (const transition of state.transitions) {
154
+ if (transition.index < index && transition.index > precedingIndex) {
155
+ preceding = transition.effort;
156
+ precedingIndex = transition.index;
157
+ }
158
+ }
159
+ if (requested === preceding) {
160
+ state.transitions = state.transitions.filter((transition) => transition.index !== index);
161
+ }
162
+ state.currentEffort = requested;
163
+ }
164
+ // Splice in ascending order so each insertion offsets only the ones after it.
165
+ state.transitions.sort((a, b) => a.index - b.index);
166
+ let offset = 0;
167
+ for (const transition of state.transitions) {
168
+ next.splice(transition.index + offset, 0, {
169
+ type: "configuration_update",
170
+ reasoning: { effort: transition.effort },
171
+ });
172
+ offset++;
173
+ }
174
+ return { input: next, result: { effort: state.baseEffort, spliced: state.transitions.length } };
175
+ }
@@ -0,0 +1,128 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { loadToolkitConfig } from "../config";
3
+ import { isExactModelAllowed } from "../model-scope";
4
+ import { CODEX_CLIENT_VERSION } from "../responses-headers";
5
+ import {
6
+ getEffortControlState,
7
+ planStableEffort,
8
+ type EffortControlState,
9
+ type PlannableInputItem,
10
+ } from "./effort-planner";
11
+
12
+ type AstraPayload = {
13
+ model: string;
14
+ input: PlannableInputItem[];
15
+ reasoning: { effort: string; [key: string]: unknown };
16
+ [key: string]: unknown;
17
+ };
18
+
19
+ function isRecord(value: unknown): value is Record<string, unknown> {
20
+ return !!value && typeof value === "object" && !Array.isArray(value);
21
+ }
22
+
23
+ /**
24
+ * Recognize the live Codex Responses request body: model + wire input array
25
+ * + a request-level string effort. Anything else (titles, embeddings, or a
26
+ * shape we do not understand) is left untouched.
27
+ */
28
+ function parseAstraPayload(payload: unknown): AstraPayload | undefined {
29
+ if (!isRecord(payload)) return undefined;
30
+ if (typeof payload.model !== "string") return undefined;
31
+ if (!Array.isArray(payload.input)) return undefined;
32
+ const reasoning = payload.reasoning;
33
+ if (!isRecord(reasoning) || typeof reasoning.effort !== "string") return undefined;
34
+ if (reasoning.effort.length === 0 || reasoning.effort === "none") return undefined;
35
+ if (!payload.input.every((item) => isRecord(item))) return undefined;
36
+ return payload as unknown as AstraPayload;
37
+ }
38
+
39
+ function isCompactionShaped(payload: AstraPayload): boolean {
40
+ // A `compaction_trigger` history belongs to a compact endpoint request,
41
+ // which rejects `configuration_update` items. Our compaction clients use
42
+ // direct fetch and never reach this hook; this guard covers any future
43
+ // provider-routed compaction.
44
+ const last = payload.input[payload.input.length - 1];
45
+ return !!last && isRecord(last) && last.type === "compaction_trigger";
46
+ }
47
+
48
+ function getSessionId(ctx: { sessionManager: { getSessionId(): string } }): string | undefined {
49
+ try {
50
+ return ctx.sessionManager.getSessionId();
51
+ } catch {
52
+ return undefined;
53
+ }
54
+ }
55
+
56
+ export function registerCodexAstraExtension(
57
+ pi: ExtensionAPI,
58
+ loadConfig: typeof loadToolkitConfig = loadToolkitConfig,
59
+ ): void {
60
+ const states = new Map<string, EffortControlState>();
61
+
62
+ // Switching or forking a session rebuilds the runtime and re-fires
63
+ // session_start; clearing there covers every replacement path.
64
+ pi.on("session_start", () => {
65
+ states.clear();
66
+ });
67
+
68
+ pi.on("before_provider_request", (event, ctx) => {
69
+ try {
70
+ const { config } = loadConfig();
71
+ const astra = config.codexAstra;
72
+ if (!astra.enabled || astra.models.length === 0) return undefined;
73
+
74
+ const model = ctx.model;
75
+ // Both Responses wire families carry the same reasoning.effort prefix
76
+ // problem, and upstream OMP plans on both providers; which models
77
+ // accept the item is decided by the allowlist, not by API family.
78
+ if (!model || (model.api !== "openai-codex-responses" && model.api !== "openai-responses")) {
79
+ return undefined;
80
+ }
81
+ if (!isExactModelAllowed(model, astra.models)) return undefined;
82
+
83
+ const payload = parseAstraPayload(event.payload);
84
+ // The payload must belong to the model the eligibility check just
85
+ // passed; a mismatch means this request is not ours to plan.
86
+ if (!payload || payload.model !== model.id) return undefined;
87
+ if (isCompactionShaped(payload)) return undefined;
88
+
89
+ const sessionId = getSessionId(ctx);
90
+ const state = getEffortControlState(
91
+ states,
92
+ `${model.baseUrl ?? ""}\u0000${model.id}\u0000${sessionId ?? "no-session"}`,
93
+ );
94
+
95
+ // Idempotence: never plan on top of items a previous pass injected.
96
+ const history = payload.input.filter((item) => item.type !== "configuration_update");
97
+ const { input, result } = planStableEffort(state, history, payload.reasoning.effort);
98
+ if (result.effort === payload.reasoning.effort && result.spliced === 0) return undefined;
99
+
100
+ return {
101
+ ...payload,
102
+ input,
103
+ reasoning: { ...payload.reasoning, effort: result.effort },
104
+ };
105
+ } catch {
106
+ // A planner failure must never break the provider request path.
107
+ return undefined;
108
+ }
109
+ });
110
+
111
+ pi.on("before_provider_headers", (event, ctx) => {
112
+ try {
113
+ const model = ctx.model;
114
+ if (!model || model.api !== "openai-codex-responses") return;
115
+ // Live requests need the same backend version gate as our synthetic
116
+ // ones: an old or missing `version` never reaches gated SKUs.
117
+ if (!event.headers.version) {
118
+ event.headers.version = CODEX_CLIENT_VERSION;
119
+ }
120
+ } catch {
121
+ // Header injection is best-effort.
122
+ }
123
+ });
124
+ }
125
+
126
+ export default function codexAstraExtension(pi: ExtensionAPI): void {
127
+ registerCodexAstraExtension(pi);
128
+ }
@@ -1,5 +1,6 @@
1
1
  import { writeDebugArtifact } from "./debug";
2
2
  import { buildResponsesRequestHeaders } from "./responses-headers";
3
+ import { stripConfigurationUpdateItems } from "./remote-v2-client";
3
4
  import type { NativeCompactionRuntime } from "./runtime";
4
5
  import type { NativeCompactionRequestBody } from "./serializer";
5
6
  import type { ArtifactContext, CompactionConfig } from "./types";
@@ -114,7 +115,10 @@ export function extractCompactedSummaryText(output: readonly unknown[]): string
114
115
 
115
116
  function toHeaders(runtime: NativeCompactionRuntime): Record<string, string> {
116
117
  return Object.fromEntries(
117
- buildResponsesRequestHeaders(runtime, { accept: JSON_CONTENT_TYPE }).entries(),
118
+ buildResponsesRequestHeaders(runtime, {
119
+ accept: JSON_CONTENT_TYPE,
120
+ sessionId: runtime.sessionId,
121
+ }).entries(),
118
122
  );
119
123
  }
120
124
 
@@ -133,7 +137,13 @@ function writeCompactArtifact(
133
137
  export async function executeNativeCompaction(
134
138
  options: ExecuteNativeCompactionOptions,
135
139
  ): Promise<NativeCompactionClientResult> {
136
- const { runtime, request, signal, settings, context } = options;
140
+ const { runtime, signal, settings, context } = options;
141
+ // Same Astra-history rejection as remote v2: `/responses/compact` rejects
142
+ // `configuration_update` items, so a replayed history never carries one.
143
+ const request: NativeCompactionRequestBody = {
144
+ ...options.request,
145
+ input: stripConfigurationUpdateItems(options.request.input) as NativeCompactionRequestBody["input"],
146
+ };
137
147
  const headers = toHeaders(runtime);
138
148
 
139
149
  if (signal?.aborted) {
package/src/config.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  DEFAULT_AUTO_COMPACTION_CONFIG,
9
9
  DEFAULT_AUTO_MODE_CONFIG,
10
10
  DEFAULT_IMAGE_GENERATION_CONFIG,
11
+ DEFAULT_CODEX_ASTRA_CONFIG,
11
12
  DEFAULT_NATIVE_FALLBACK_CONFIG,
12
13
  DEFAULT_WEB_SEARCH_CONFIG,
13
14
  RESPONSES_COMPACT_CAPABLE_APIS,
@@ -28,6 +29,7 @@ import {
28
29
  type AutoModeClassifierConfig,
29
30
  type AutoModeConfig,
30
31
  type AutoModeGate,
32
+ type CodexAstraConfig,
31
33
  type CompactionConfig,
32
34
  type ImageGenerationConfig,
33
35
  type LoadedToolkitConfig,
@@ -39,7 +41,7 @@ import {
39
41
  export const CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", TOOLKIT_ID);
40
42
  export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
41
43
 
42
- const TOP_LEVEL_FIELDS = new Set(["compaction", "webSearch", "imageGeneration", "autoMode"]);
44
+ const TOP_LEVEL_FIELDS = new Set(["compaction", "webSearch", "imageGeneration", "autoMode", "codexAstra"]);
43
45
  const COMPACTION_FIELDS = new Set([
44
46
  "enabled",
45
47
  "allowCompactionContinuityBreak",
@@ -58,6 +60,7 @@ const NATIVE_FALLBACK_FIELDS = new Set(["enabled", "model", "thinkingLevel"]);
58
60
  const AUTO_COMPACTION_FIELDS = new Set(["enabled", "continuation", "unsupportedFallback", "reserveTokens"]);
59
61
  const WEB_SEARCH_FIELDS = new Set(["enabled", "models"]);
60
62
  const IMAGE_GENERATION_FIELDS = new Set(["enabled"]);
63
+ const CODEX_ASTRA_FIELDS = new Set(["enabled", "models"]);
61
64
  const AUTO_MODE_FIELDS = new Set([
62
65
  "enabled",
63
66
  "models",
@@ -269,6 +272,10 @@ function cloneDefaults(): ToolkitConfig {
269
272
  models: [...DEFAULT_WEB_SEARCH_CONFIG.models],
270
273
  },
271
274
  imageGeneration: { ...DEFAULT_IMAGE_GENERATION_CONFIG },
275
+ codexAstra: {
276
+ ...DEFAULT_CODEX_ASTRA_CONFIG,
277
+ models: [...DEFAULT_CODEX_ASTRA_CONFIG.models],
278
+ },
272
279
  autoMode: {
273
280
  ...DEFAULT_AUTO_MODE_CONFIG,
274
281
  models: [...DEFAULT_AUTO_MODE_CONFIG.models],
@@ -417,6 +424,20 @@ function applyImageGenerationConfig(
417
424
  toBoolean(raw.enabled, "imageGeneration.enabled", warnings) ?? resolved.enabled;
418
425
  }
419
426
 
427
+ function applyCodexAstraConfig(
428
+ raw: Record<string, unknown>,
429
+ resolved: CodexAstraConfig,
430
+ warnings: string[],
431
+ ): void {
432
+ warnUnknownFields(raw, CODEX_ASTRA_FIELDS, "codexAstra", warnings);
433
+ resolved.enabled = toBoolean(raw.enabled, "codexAstra.enabled", warnings) ?? resolved.enabled;
434
+
435
+ const models = toStringList(raw.models, "codexAstra.models", warnings);
436
+ if (models !== undefined) {
437
+ resolved.models = models;
438
+ }
439
+ }
440
+
420
441
  function applyAutoModeConfig(
421
442
  raw: Record<string, unknown>,
422
443
  resolved: AutoModeConfig,
@@ -604,6 +625,14 @@ export function loadToolkitConfig(configPath: string = CONFIG_PATH): LoadedToolk
604
625
  warnings.push("Ignoring autoMode: expected a JSON object.");
605
626
  }
606
627
  }
628
+
629
+ if (raw.codexAstra !== undefined) {
630
+ if (isRecord(raw.codexAstra)) {
631
+ applyCodexAstraConfig(raw.codexAstra, resolved.codexAstra, warnings);
632
+ } else {
633
+ warnings.push("Ignoring codexAstra: expected a JSON object.");
634
+ }
635
+ }
607
636
  }
608
637
 
609
638
  resolved.compaction.artifactRoot = resolveConfiguredPath(
@@ -119,16 +119,32 @@ function toHeaders(runtime: NativeCompactionRuntime): Record<string, string> {
119
119
  buildResponsesRequestHeaders(runtime, {
120
120
  accept: SSE_CONTENT_TYPE,
121
121
  contentType: JSON_CONTENT_TYPE,
122
+ sessionId: runtime.sessionId,
122
123
  }).entries(),
123
124
  );
124
125
  }
125
126
 
127
+ /**
128
+ * Codex `/responses` rejects histories that mix this Astra-only item into a
129
+ * compaction request (verified upstream in Oh My Pi: the compaction path is
130
+ * deliberately built outside the effort planner). The host never emits the
131
+ * item into a payload we would replay today, but the item is transparently
132
+ * carried by our opaque-window replay, so strip it defensively on every
133
+ * remote v2 request regardless of API family.
134
+ */
135
+ export function stripConfigurationUpdateItems(input: readonly unknown[]): unknown[] {
136
+ return input.filter((item) => !isRecord(item) || item["type"] !== "configuration_update");
137
+ }
138
+
126
139
  export function buildRemoteV2CompactionRequest(
127
140
  request: NativeCompactionRequestBody,
128
141
  ): RemoteV2CompactionRequestBody {
129
142
  return {
130
143
  ...structuredClone(request),
131
- input: [...structuredClone(request.input), { type: "compaction_trigger" }],
144
+ input: [
145
+ ...(stripConfigurationUpdateItems(structuredClone(request.input)) as ResponsesInputItem[]),
146
+ { type: "compaction_trigger" },
147
+ ],
132
148
  store: false,
133
149
  stream: true,
134
150
  };
@@ -5,6 +5,7 @@ export type ResponsesHeaderRuntime = {
5
5
  api: string;
6
6
  apiKey: string;
7
7
  headers?: ProviderHeaders;
8
+ sessionId?: string;
8
9
  currentModel: {
9
10
  headers?: ProviderHeaders;
10
11
  };
@@ -38,9 +39,34 @@ function buildCodexUserAgent(): string {
38
39
  return `pi (${platform}; ${arch})`;
39
40
  }
40
41
 
42
+ /**
43
+ * The Codex backend version-gates model availability on the `version` header
44
+ * (the pinned @openai/codex client version). `gpt-6-astra` requires >= 0.153.0;
45
+ * an older or absent pin makes the SKU invisible to discovery and rejects
46
+ * requests that name it. Keep this in sync with the newest gate Oh My Pi pins
47
+ * in @oh-my-pi/pi-wire/codex (`CODEX_CLIENT_VERSION`).
48
+ */
49
+ export const CODEX_CLIENT_VERSION = "0.153.0";
50
+
51
+ /** Prompt cache keys are clamped by the same host rule (`OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH`). */
52
+ const CODEX_SESSION_ID_MAX_LENGTH = 64;
53
+
54
+ export function clampCodexSessionId(sessionId: string | undefined): string | undefined {
55
+ if (!sessionId) return undefined;
56
+ const chars = Array.from(sessionId);
57
+ return chars.length <= CODEX_SESSION_ID_MAX_LENGTH ? sessionId : chars.slice(0, CODEX_SESSION_ID_MAX_LENGTH).join("");
58
+ }
59
+
60
+ export type ResponsesHeaderOptions = {
61
+ accept: string;
62
+ contentType?: string;
63
+ /** Conversation identity for Codex cache-affinity headers; clamped to 64 chars here. */
64
+ sessionId?: string;
65
+ };
66
+
41
67
  export function buildResponsesRequestHeaders(
42
68
  runtime: ResponsesHeaderRuntime,
43
- options: { accept: string; contentType?: string },
69
+ options: ResponsesHeaderOptions,
44
70
  ): Headers {
45
71
  const headers = new Headers(
46
72
  mergeProviderHeaders(runtime.currentModel.headers, runtime.headers),
@@ -57,6 +83,21 @@ export function buildResponsesRequestHeaders(
57
83
  headers.set("originator", "pi");
58
84
  headers.set("user-agent", buildCodexUserAgent());
59
85
  headers.set("openai-beta", "responses=experimental");
86
+ // Codex backend gating: a request without a new-enough `version` never
87
+ // reaches model SKUs that arrived behind the gate (gpt-6-astra).
88
+ headers.set("version", CODEX_CLIENT_VERSION);
89
+ // Prompt-cache affinity rides on the conversation identity: the backend
90
+ // keeps a conversation's cache prefix on the node that wrote it, so our
91
+ // synthetic compaction requests must present the same id the live
92
+ // transcript requests used, or every compact pays a full cache miss.
93
+ const sessionId = clampCodexSessionId(options.sessionId ?? runtime.sessionId);
94
+ if (sessionId) {
95
+ // Header names match the installed host's live-request builder
96
+ // (`buildSSEHeaders`: `session-id` + `x-client-request-id`) so our
97
+ // synthetic requests land in the same conversation affinity group.
98
+ headers.set("session-id", sessionId);
99
+ headers.set("x-client-request-id", sessionId);
100
+ }
60
101
  }
61
102
 
62
103
  return headers;
package/src/runtime.ts CHANGED
@@ -54,6 +54,12 @@ export type ResponsesRuntime = {
54
54
  headers?: ProviderHeaders;
55
55
  responsesPath: string;
56
56
  responsesUrl: string;
57
+ /**
58
+ * Conversation identity for Codex prompt-cache affinity headers. Synthetic
59
+ * compaction/review requests must present the same id the live transcript
60
+ * requests used or the backend treats them as a new conversation.
61
+ */
62
+ sessionId?: string;
57
63
  currentModel: RuntimeModel;
58
64
  };
59
65
 
@@ -229,6 +235,15 @@ export function getRuntimeModelDescriptor(model: RuntimeModel | undefined): {
229
235
  };
230
236
  }
231
237
 
238
+ /** Best-effort conversation id; header emission then simply skips the affinity headers. */
239
+ export function resolveRuntimeSessionId(ctx: ExtensionContext): string | undefined {
240
+ try {
241
+ return ctx.sessionManager.getSessionId();
242
+ } catch {
243
+ return undefined;
244
+ }
245
+ }
246
+
232
247
  async function resolveNativeCompactionEnvironmentForModel(
233
248
  ctx: ExtensionContext,
234
249
  currentModel: RuntimeModel | undefined,
@@ -336,6 +351,7 @@ async function resolveNativeCompactionEnvironmentForModel(
336
351
  responsesUrl: buildResponsesUrl(baseUrl, descriptor.api),
337
352
  compactPath: buildCompactPath(descriptor.api),
338
353
  compactUrl: buildCompactUrl(baseUrl, descriptor.api),
354
+ sessionId: resolveRuntimeSessionId(ctx),
339
355
  payload: requestPayload,
340
356
  currentModel,
341
357
  },
@@ -361,6 +377,7 @@ export async function resolveResponsesEnvironment(
361
377
  headers: runtime.headers,
362
378
  responsesPath: runtime.responsesPath,
363
379
  responsesUrl: runtime.responsesUrl,
380
+ sessionId: runtime.sessionId,
364
381
  currentModel: runtime.currentModel,
365
382
  },
366
383
  };
package/src/types.ts CHANGED
@@ -97,6 +97,12 @@ export type ImageGenerationConfig = {
97
97
  enabled: boolean;
98
98
  };
99
99
 
100
+ export type CodexAstraConfig = {
101
+ enabled: boolean;
102
+ /** Exact provider/model keys that receive the stable-effort `configuration_update` rewrite. */
103
+ models: string[];
104
+ };
105
+
100
106
  /** `side-effect` reviews bash/write/edit plus extras; `all` reviews every tool call. */
101
107
  export type AutoModeGate = "side-effect" | "all";
102
108
 
@@ -162,6 +168,7 @@ export type ToolkitConfig = {
162
168
  webSearch: WebSearchConfig;
163
169
  imageGeneration: ImageGenerationConfig;
164
170
  autoMode: AutoModeConfig;
171
+ codexAstra: CodexAstraConfig;
165
172
  };
166
173
 
167
174
  export type LoadedToolkitConfig = {
@@ -524,6 +531,11 @@ export const DEFAULT_IMAGE_GENERATION_CONFIG: ImageGenerationConfig = {
524
531
  enabled: false,
525
532
  };
526
533
 
534
+ export const DEFAULT_CODEX_ASTRA_CONFIG: CodexAstraConfig = {
535
+ enabled: false,
536
+ models: [],
537
+ };
538
+
527
539
  export const DEFAULT_AUTO_MODE_CONFIG: AutoModeConfig = {
528
540
  enabled: true,
529
541
  models: [],
@@ -552,4 +564,5 @@ export const DEFAULT_TOOLKIT_CONFIG: ToolkitConfig = {
552
564
  webSearch: DEFAULT_WEB_SEARCH_CONFIG,
553
565
  imageGeneration: DEFAULT_IMAGE_GENERATION_CONFIG,
554
566
  autoMode: DEFAULT_AUTO_MODE_CONFIG,
567
+ codexAstra: DEFAULT_CODEX_ASTRA_CONFIG,
555
568
  };