pi-openai-toolkit 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  ---
@@ -133,7 +139,27 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
133
139
  "extraTools": [
134
140
  "openai_generate_image"
135
141
  ],
136
- "timeoutMs": 30000
142
+ "timeoutMs": 30000,
143
+ "transcript": true,
144
+ "evidenceTools": true,
145
+ "maxEvidenceRounds": 3,
146
+ "classifier": {
147
+ "enabled": false,
148
+ "model": "uwoacrimson/gpt-5.6-luna",
149
+ "timeoutMs": 15000,
150
+ "maxLag": 2
151
+ },
152
+ "circuitBreaker": {
153
+ "consecutiveDenials": 3,
154
+ "recentDenials": 10,
155
+ "windowSize": 50
156
+ }
157
+ },
158
+ "codexAstra": {
159
+ "enabled": true,
160
+ "models": [
161
+ "openai-codex/gpt-6-astra"
162
+ ]
137
163
  }
138
164
  }
139
165
  ```
@@ -179,10 +205,23 @@ The request uses the current model's resolved Responses endpoint, authentication
179
205
  - `reviewerModel` (*string | null*, default: `null`): The approving model, formatted as `provider/model-id`. It is resolved through Pi's model registry and must exist there; it is used only for one-off review requests and never becomes the active conversation model. Auto mode cannot engage while this is unset.
180
206
  - `gate` (*`"side-effect" | "all"`*, default: `"side-effect"`): `side-effect` reviews `bash`, `write`, and `edit`; `all` reviews every tool call, including read-only tools.
181
207
  - `extraTools` (*string[]*, default: `[]`): Additional tool names reviewed on top of the `side-effect` default set, for example `openai_generate_image` or an MCP tool name.
182
- - `timeoutMs` (*integer*, default: `30000`, range `1000`-`120000`): Reviewer deadline. Expiry never becomes approval.
208
+ - `timeoutMs` (*integer*, default: `30000`, range `1000`-`120000`): Reviewer deadline across all of its rounds. Expiry never becomes approval.
209
+ - `transcript` (*boolean*, default: `true`): Send the reviewer a bounded view of the conversation. Without it the reviewer can only judge the action's intrinsic risk and must treat user authorization as `unknown`.
210
+ - `evidenceTools` (*boolean*, default: `true`): Let the reviewer spend read-only tool calls (`read`, `grep`, `find`, `ls`) establishing facts before it decides. These are Pi's own read-only tools executed by this extension; the reviewer is never given `bash`, `write`, `edit`, or network access.
211
+ - `maxEvidenceRounds` (*integer*, default: `3`, range `0`-`8`): How many investigation rounds the reviewer may take before it must answer. `0` disables tools and keeps a single completion.
212
+ - `classifier.enabled` (*boolean*, default: `false`): Opt in to a non-blocking trajectory pre-scorer that can satisfy an ordinary gated call from a cached low-risk classification instead of waiting for a full review. It is off by default because it adds one model call per gated tool result.
213
+ - `classifier.model` (*string | null*): Classifier model as `provider/model-id`; falls back to `reviewerModel`. A classifier only ever defers to the blocking reviewer; it cannot approve on its own beyond a cached low-risk fast path, and it never denies.
214
+ - `classifier.timeoutMs` (*integer*, default: `15000`) and `classifier.maxLag` (*integer*, default: `2`, range `0`-`20`): A score older than `maxLag` tool calls, or older than the last user turn, is stale and forces the blocking review.
215
+ - `circuitBreaker.consecutiveDenials` (*integer*, default: `3`, `0` disables): Reviewer denials in a row that end the turn instead of letting the agent keep negotiating.
216
+ - `circuitBreaker.recentDenials` (*integer*, default: `10`, `0` disables) and `circuitBreaker.windowSize` (*integer*, default: `50`, range `1`-`200`): Denials inside the last N reviewed calls that end the turn. This catches a scattered denial rate that never forms a consecutive run.
183
217
 
184
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.
185
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. Today that is only `["openai-codex/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
+
186
225
  ---
187
226
 
188
227
  ## Core Mechanics
@@ -253,13 +292,31 @@ Pi does not prompt before running `bash`, `write`, or `edit`; the only built-in
253
292
 
254
293
  - **Engagement is non-blocking**: `pi --auto`, `/auto on`, and `/auto all` only flip in-session state and update the footer status. No provider request happens at engagement time, so turning the mode on can never stall the current turn.
255
294
  - **Gate scope**: `side-effect` reviews `bash`, `write`, and `edit`; `extraTools` adds named tools; `/auto all` (or `gate: "all"`) reviews every tool call. Read-only tools pass through untouched in the default scope.
256
- - **One review per gated call**: The reviewer receives the tool name, its arguments, the most recent user request as intent, and the working directory. Tool arguments are labelled untrusted data, so text inside them cannot instruct the reviewer.
257
- - **Strict verdict contract**: The reviewer must answer with a single `{"decision":"allow"|"deny","reason":"..."}` object. Anything unparseable, an unknown decision, a provider error, a timeout, or a cancellation is reported as *unavailable* and never treated as approval.
258
- - **Deny feeds the agent**: A denial blocks the tool and returns the reviewer's reason to the requesting model, which can then choose a different approach instead of stalling.
295
+ - **Two independent axes, one derived outcome**: The reviewer scores the action's intrinsic risk (`low`/`medium`/`high`/`critical`) separately from how far the conversation actually authorizes it (`unknown`/`low`/`medium`/`high`), then derives `allow`/`deny` from a fixed table: `low` and `medium` risk are allowed, `high` requires at least `medium` authorization and a narrow scope, `critical` is always denied. Judging only "does this command look dangerous" is what makes an approval reviewer rubber-stamp or block everything.
296
+ - **Bounded transcript**: The reviewer receives a compact view of the conversation built from Pi's session entries, with user turns protected under their own budget and tool output on a smaller separate one, so verbose command output cannot crowd out the human turns that establish authorization. Omitted history is marked `<truncated />` rather than silently dropped, and only `[user]` entries count as evidence of approval.
297
+ - **The reviewer can investigate**: When `evidenceTools` is on, the reviewer runs as a short bounded agent loop over Pi's read-only tools (`read`, `grep`, `find`, `ls`). It can open the script it is about to allow, check whether a deletion target exists or is empty, or confirm a write path lies inside the project. This extension executes those calls; the reviewer is never handed `bash`, a write tool, or network access, which is what keeps "let the judge look around" safe.
298
+ - **Lenient verdict contract**: Only the outcome is required. `{"outcome":"allow"}` is a complete answer; risk, authorization, and rationale are back-filled (an unstated risk on a deny is recorded as `high`, which keeps the audit conservative in the direction that matters). Both `outcome`/`rationale` and the older `decision`/`reason` spelling are accepted. Unreadable output is a failure, never a guess.
299
+ - **Untrusted evidence stays untrusted**: Tool arguments, transcript text, and file contents are labelled as data. Claims of prior approval, urgency, or instructions to answer a particular way inside them are ignored by policy.
300
+ - **Optional non-blocking pre-scorer**: With `classifier.enabled`, a cheap single-token classification (`high`/`low`) of the trajectory runs after each gated tool result. A fresh `low` score satisfies the next gated call without waiting for a full review; `high`, a stale score, a failed sample, or a repeat of a tool already denied this turn all fall through to the blocking reviewer. The classifier can never deny anything.
301
+ - **Denials carry an anti-circumvention clause**: A block returns the risk, the authorization, and the reviewer's reason to the requesting model, plus an explicit prohibition on achieving the same outcome by workaround, renaming, splitting, or indirect execution. The model may take a materially safer alternative or ask you.
302
+ - **A review that did not finish is not a safety verdict**: Timeout, cancellation, provider error, and unreadable output each report their own cause and tell the agent not to infer unsafety from the failure. Only an actual `deny` is worded as a rejection.
303
+ - **Rejection circuit breaker**: Repeated denials in one turn end the turn rather than letting the agent and reviewer negotiate indefinitely, on either a consecutive run or a denial rate inside a sliding window. `turn_start` clears the counters.
259
304
  - **Human fallback**: When a review is unavailable in an interactive session, the extension asks you directly with the failure reason. In print (`-p`) and JSON mode there is nobody to ask, so the action fails closed and is blocked.
260
305
  - **Eligibility is rechecked live**: Switching to a model outside `autoMode.models`, clearing `reviewerModel`, or setting `enabled: false` disengages immediately. Returning to an allowlisted model does not re-engage automatically; you must ask again with `/auto on`.
261
- - **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 reason, reviewer model, and decision source. Custom entries never enter LLM context, and tool arguments are not recorded.
262
- - **Cost boundary**: Each gated call adds one reviewer completion, so auto mode consumes model tokens. The reviewer runs with prompt caching disabled and no tools, and it cannot execute anything itself.
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.
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.
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` (Astra is the only SKU that accepts the item; anything else answers HTTP 400 on 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.
263
320
 
264
321
  ---
265
322
 
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。
@@ -133,7 +137,27 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
133
137
  "extraTools": [
134
138
  "openai_generate_image"
135
139
  ],
136
- "timeoutMs": 30000
140
+ "timeoutMs": 30000,
141
+ "transcript": true,
142
+ "evidenceTools": true,
143
+ "maxEvidenceRounds": 3,
144
+ "classifier": {
145
+ "enabled": false,
146
+ "model": "uwoacrimson/gpt-5.6-luna",
147
+ "timeoutMs": 15000,
148
+ "maxLag": 2
149
+ },
150
+ "circuitBreaker": {
151
+ "consecutiveDenials": 3,
152
+ "recentDenials": 10,
153
+ "windowSize": 50
154
+ }
155
+ },
156
+ "codexAstra": {
157
+ "enabled": true,
158
+ "models": [
159
+ "openai-codex/gpt-6-astra"
160
+ ]
137
161
  }
138
162
  }
139
163
  ```
@@ -179,10 +203,23 @@ C:\Users\<user>\.pi\agent\extensions\pi-openai-toolkit\config.json
179
203
  - `reviewerModel` (*string | null*, 默认 `null`): 执行审批的模型,格式为 `"provider/model-id"`。它通过 Pi 的模型注册表解析且必须存在,仅用于一次性审批请求,不会成为当前会话模型。未设置时 Auto 模式无法启用。
180
204
  - `gate` (*`"side-effect" | "all"`*, 默认 `"side-effect"`): `side-effect` 只审批 `bash`、`write`、`edit`;`all` 审批包括只读工具在内的全部工具调用。
181
205
  - `extraTools` (*string[]*, 默认 `[]`): 在 `side-effect` 默认集之上额外审批的工具名,例如 `openai_generate_image` 或某个 MCP 工具名。
182
- - `timeoutMs` (*整数*, 默认 `30000`,范围 `1000`–`120000`): 审批超时时间。超时永远不会被当作放行。
206
+ - `timeoutMs` (*整数*, 默认 `30000`,范围 `1000`–`120000`): 审批模型所有轮次共用的总超时。超时永远不会被当作放行。
207
+ - `transcript` (*boolean*, 默认 `true`): 给审批模型一份有界的会话上下文。关掉后它只能判断动作本身的风险,用户授权程度只能记为 `unknown`。
208
+ - `evidenceTools` (*boolean*, 默认 `true`): 允许审批模型在结论之前调用只读工具(`read`、`grep`、`find`、`ls`)自己取证据。这些工具由本扩展执行;审批模型永远拿不到 `bash`、写入类工具或网络访问。
209
+ - `maxEvidenceRounds` (*整数*, 默认 `3`,范围 `0`–`8`): 最多允许几轮取证,超过就必须给结论。设为 `0` 完全关闭工具,回到单次补全。
210
+ - `classifier.enabled` (*boolean*, 默认 `false`): 是否启用非阻塞的轨迹预评分。开启后一个新鲜的 `low` 分数可以直接满足下一次受控调用,不必等完整审批。默认关闭是因为每个受控工具结果都会多一次模型调用。
211
+ - `classifier.model` (*string | null*): 预评分模型,格式 `provider/model-id`,未设置时沿用 `reviewerModel`。预评分只能把调用退回阻塞审批;除了缓存的 `low` 快速通道之外它无权放行,也永不拒绝。
212
+ - `classifier.timeoutMs` (*整数*, 默认 `15000`) 与 `classifier.maxLag` (*整数*, 默认 `2`,范围 `0`–`20`): 经过超过 `maxLag` 个工具调用、或用户又说了新话之后,分数作废并强制走阻塞审批。
213
+ - `circuitBreaker.consecutiveDenials` (*整数*, 默认 `3`,`0` 关闭): 单轮内连续拒绝多少次就直接结束这一轮,而不是让主模型无限拉锯。
214
+ - `circuitBreaker.recentDenials` (*整数*, 默认 `10`,`0` 关闭) 与 `circuitBreaker.windowSize` (*整数*, 默认 `50`,范围 `1`–`200`): 最近 N 次受审调用里拒绝多少次就结束这一轮。它抓的是没有连续出现但密度很高的拒绝。
183
215
 
184
216
  Auto 模式需要显式启用(`models` 默认为空),因为它相当于允许一个模型代你批准文件写入和命令执行。会话内可用 `pi --auto` 或 `/auto on` 开启,`/auto all` 一次审批全部工具。具体判定与降级规则见「核心工作原理解析」第 5 节。
185
217
 
218
+ #### `codexAstra`(Codex GPT-6 Astra 适配配置)
219
+
220
+ - `enabled` (*boolean*, 默认 `false`): Astra 适配层总开关:实时请求的版本门禁头,以及「核心工作原理解析」第 6 节描述的 stable-effort 改写。
221
+ - `models` (*string[]*, 默认 `[]`): 接受 `configuration_update` 会话项的精确 `provider/model-id` 白名单。目前只有 `["openai-codex/gpt-6-astra"]`;其他模型绝不会被改写。版本门禁头则对所有 `openai-codex-responses` 实时请求生效,不受这个列表限制。
222
+
186
223
  ---
187
224
 
188
225
  ## 核心工作原理解析
@@ -253,13 +290,31 @@ Pi 本身在运行 `bash`、`write`、`edit` 前不会询问确认,内置的
253
290
 
254
291
  - **开启动作不阻塞任务流**:`pi --auto`、`/auto on`、`/auto all` 只切换会话内状态并更新底部状态栏,开启时不会发起任何 Provider 请求。
255
292
  - **审批范围**:`side-effect` 审批 `bash`、`write`、`edit`;`extraTools` 追加工具名;`/auto all`(或 `gate: "all"`)审批全部工具调用。默认范围下只读工具直接放行。
256
- - **每个受审调用一次审批**:审批模型收到工具名、完整参数、最近一次用户请求作为意图、以及工作目录。工具参数被标记为不可信数据,其中的文本无法反过来指挥审批模型。
257
- - **严格结论契约**:审批模型必须只回答一个 `{"decision":"allow"|"deny","reason":"..."}` 对象。无法解析、未知结论、Provider 报错、超时或取消都记为 *unavailable*,绝不视为放行。
258
- - **拒绝回喂主模型**:deny 会拦截该工具并把理由返回给发起调用的模型,让它改走其他方案而不是卡死。
293
+ - **两个独立轴,一个推导结论**:审批模型分别给出动作本身的风险(`low`/`medium`/`high`/`critical`)和会话对它的授权程度(`unknown`/`low`/`medium`/`high`),再按固定表格推出 `allow`/`deny`:`low` 与 `medium` 风险放行,`high` 需要至少 `medium` 授权且范围窄,`critical` 一律拒绝。只问“这条命令危不危险”正是审批模型要么全部放行、要么全部拦截的原因。
294
+ - **有界会话上下文**:审批模型收到的是从 Pi 会话条目重建的紧凑上下文。用户发言有自己独立的预算并优先保留,工具输出用另一份更小的预算,避免冗长命令输出挤掉真正能建立授权证据的人类发言。被省略的历史会标 `<truncated />` 而不是静默丢掉,且只有 `[user]` 条目能作为授权证据。
295
+ - **审批模型可以自己取证**:`evidenceTools` 开启后,审批模型跑在一个有界的只读工具循环里(`read`、`grep`、`find`、`ls`)。它可以打开准备放行的脚本看它到底做什么、检查删除目标是否存在或为空、确认写入路径是否在项目内。这些调用由本扩展执行,审批模型永远拿不到 `bash` 与写入工具,也不会联网,这是“让裁判自己看一眼”能安全成立的前提。
296
+ - **宽松结论契约**:只有结论字段是必需的。`{"outcome":"allow"}` 就是一个完整回答,风险、授权、理由由代码回填(拒绝但未说风险时按 `high` 记录,让审计往保守方向偏)。`outcome`/`rationale` 与旧的 `decision`/`reason` 两种写法都接受。解析失败是失败,不是猜测。
297
+ - **不可信证据保持不可信**:工具参数、上下文文本、文件内容都标记为数据。其中出现的“用户已经同意过”、“很紧急”、或要求按某种方式回答的指令,按策略一律忽略。
298
+ - **可选的非阻塞预评分**:`classifier.enabled` 后,每个受控工具结果之后会跑一次单 token 的轨迹分类(`high`/`low`)。新鲜的 `low` 可以直接满足下一次受控调用;`high`、过期分数、评分失败、以及本轮被拒绝过的同一工具重试,全部退回阻塞审批。预评分自己永不拒绝。
299
+ - **拒绝带反规避条款**:拦截时会把风险等级、授权程度、理由一起返回给主模型,并附上明确禁止:不得通过变通、改名、拆分命令或间接执行达成同一结果。它只能换实质更安全的方案,或者来问你。
300
+ - **没跑完的审批不是安全结论**:超时、取消、Provider 报错、输出不合法各自上报 cause,并告诉主模型不要因为这个失败就推断动作不安全。只有真正的 `deny` 才会被表述为拒绝。
301
+ - **拒绝熔断器**:单轮内反复被拒会直接结束这一轮,而不是让主模型和审批模型无限拉锯;连续次数和滑动窗口内的拒绝密度两个条件都会触发。`turn_start` 会清空计数。
259
302
  - **降级人工确认**:交互会话中审批不可用时,扩展会带着失败原因直接问你;而 `-p` 与 JSON 模式无人可问,因此 fail-closed 直接拦截。
260
303
  - **实时重新校验资格**:切到 `autoMode.models` 之外的模型、清空 `reviewerModel` 或设 `enabled: false` 会立即退出 Auto 模式;切回白名单模型不会自动恢复,必须重新 `/auto on`。
261
- - **有界审计记录**:每次判定都以自定义会话条目(`pi-openai-toolkit.auto-mode-decision.v1`)落盘,包含工具名、调用 id、结论、有界理由、审批模型和判定来源。自定义条目不进入 LLM 上下文,且不记录工具参数。
262
- - **费用边界**:每个受审调用额外产生一次审批补全,因此 Auto 模式会消耗模型 Token。审批请求禁用提示缓存、不携带任何工具,且自身无法执行操作。
304
+ - **有界审计记录**:每次判定都以自定义会话条目(`pi-openai-toolkit.auto-mode-decision.v1`)落盘,包含工具名、调用 id、结论、有界理由、审批模型、风险等级、用户授权程度、取证轮数,以及判定来源(`reviewer`、`classifier`、`human`、`policy` 或 `circuit-breaker`)。自定义条目不进入 LLM 上下文,且不记录工具参数。
305
+ - **费用边界**:每个受控调用至少产生一次审批补全;开启 `evidenceTools` 后,这一次审批最多可膨胀为 `maxEvidenceRounds` 轮额外补全。开启预评分时,每个受控工具结果多一次小补全。审批请求禁用提示缓存,除上述只读工具外不能执行任何操作。
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
+ - **适用范围**:改写只作用于 `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` 读凭据,不碰存储文件。
263
318
 
264
319
  ---
265
320
 
@@ -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.9.0",
3
+ "version": "0.11.0",
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": [
@@ -60,8 +61,14 @@
60
61
  "src/image-generation/types.ts",
61
62
  "src/auto-mode/extension.ts",
62
63
  "src/auto-mode/policy.ts",
64
+ "src/auto-mode/prompt.ts",
65
+ "src/auto-mode/transcript.ts",
63
66
  "src/auto-mode/reviewer.ts",
67
+ "src/auto-mode/classifier.ts",
64
68
  "src/auto-mode/types.ts",
69
+ "extensions/codex-astra.ts",
70
+ "src/codex-astra/effort-planner.ts",
71
+ "src/codex-astra/extension.ts",
65
72
  "src/model-scope.ts",
66
73
  "src/native-fallback.ts",
67
74
  "src/payload-rewrite.ts",
@@ -0,0 +1,165 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
3
+ import { parseModelSpec } from "../runtime";
4
+ import { CLASSIFIER_SYSTEM_PROMPT } from "./prompt";
5
+ import { boundReviewText, type ClassifierRisk, type FastDecisionReason } from "./types";
6
+
7
+ /** One completed classification of the trajectory, tagged to a tool-call position. */
8
+ export type ScoreRecord = {
9
+ risk: ClassifierRisk;
10
+ /** Tool-call counter value when the sample was taken. */
11
+ scoredAtCall: number;
12
+ /** Fingerprint of the user turns the score was based on. */
13
+ authorizationVersion: string;
14
+ sampledAt: number;
15
+ };
16
+
17
+ /**
18
+ * Mutable per-session tracker for the non-blocking pre-scorer.
19
+ *
20
+ * The counters reproduce Codex's staleness rule: the agent keeps running while a
21
+ * classification is in flight, so a score is only trustworthy if it is recent
22
+ * enough and nothing the user said has changed underneath it.
23
+ */
24
+ export type ScoreTracker = {
25
+ latestCallIndex: number;
26
+ latestScoredIndex: number;
27
+ latestFailedIndex: number;
28
+ score?: ScoreRecord;
29
+ };
30
+
31
+ export function createScoreTracker(): ScoreTracker {
32
+ return { latestCallIndex: 0, latestScoredIndex: 0, latestFailedIndex: 0 };
33
+ }
34
+
35
+ /** Reset everything: a new turn or a disengaged mode invalidates prior scoring. */
36
+ export function resetScoreTracker(tracker: ScoreTracker): void {
37
+ tracker.latestCallIndex = 0;
38
+ tracker.latestScoredIndex = 0;
39
+ tracker.latestFailedIndex = 0;
40
+ tracker.score = undefined;
41
+ }
42
+
43
+ /**
44
+ * Fingerprint of the user-authored conversation. Any new or edited user turn makes
45
+ * an existing score stale, because the score's authorization premise changed.
46
+ */
47
+ export function authorizationVersion(userText: string): string {
48
+ const bounded = boundReviewText(userText, 8_000);
49
+ let hash = 2166136261;
50
+ for (let index = 0; index < bounded.length; index += 1) {
51
+ hash ^= bounded.charCodeAt(index);
52
+ hash = Math.imul(hash, 16777619);
53
+ }
54
+ return `${bounded.length}:${(hash >>> 0).toString(36)}`;
55
+ }
56
+
57
+ export function recordScoredCall(tracker: ScoreTracker, score: ScoreRecord): void {
58
+ tracker.score = score;
59
+ tracker.latestScoredIndex = Math.max(tracker.latestScoredIndex, score.scoredAtCall);
60
+ }
61
+
62
+ export function recordFailedCall(tracker: ScoreTracker, scoredAtCall: number): void {
63
+ tracker.latestFailedIndex = Math.max(tracker.latestFailedIndex, scoredAtCall);
64
+ }
65
+
66
+ export type FastDecisionInput = {
67
+ tracker: ScoreTracker;
68
+ /** Tool-call counter value for the call being gated. */
69
+ currentCallIndex: number;
70
+ authorizationVersion: string;
71
+ maxLag: number;
72
+ /** Retries after a denial and widened gates must not take the fast path. */
73
+ requiresSynchronousReview?: boolean;
74
+ };
75
+
76
+ /**
77
+ * Decide whether a cached classification can satisfy this gated call without
78
+ * running the blocking reviewer. Anything other than `low_risk` defers to the
79
+ * synchronous review; the classifier never denies an action on its own.
80
+ */
81
+ export function fastApprovalEligible(input: FastDecisionInput): {
82
+ eligible: boolean;
83
+ reason: FastDecisionReason;
84
+ } {
85
+ const defer = (reason: FastDecisionReason) => ({ eligible: false, reason });
86
+
87
+ if (input.requiresSynchronousReview) return defer("requires_synchronous_review");
88
+ if (input.tracker.latestFailedIndex > input.tracker.latestScoredIndex) return defer("scoring_failure");
89
+
90
+ const score = input.tracker.score;
91
+ if (!score) return defer("missing_score");
92
+
93
+ const lag = input.currentCallIndex - score.scoredAtCall;
94
+ if (lag > input.maxLag) return defer("stale_score");
95
+ if (score.authorizationVersion !== input.authorizationVersion) return defer("authorization_changed");
96
+ if (score.risk === "low") return { eligible: true, reason: "low_risk" };
97
+ return defer("elevated_risk");
98
+ }
99
+
100
+ export type ClassificationResult =
101
+ | { kind: ClassifierRisk; model: string }
102
+ | { kind: "failed"; reason: string };
103
+
104
+ /**
105
+ * Run one non-blocking classification. Output is a single token; anything else is a
106
+ * failed sample rather than a verdict, so a confused classifier costs a review and
107
+ * never an approval or a denial.
108
+ */
109
+ export async function classifyTrajectory(params: {
110
+ registry: Pick<ModelRegistry, "find" | "complete">;
111
+ modelSpec: string;
112
+ prompt: string;
113
+ timeoutMs: number;
114
+ signal?: AbortSignal;
115
+ }): Promise<ClassificationResult> {
116
+ const parsed = parseModelSpec(params.modelSpec);
117
+ if (!parsed) {
118
+ return { kind: "failed", reason: `autoMode.classifier.model "${params.modelSpec}" is not provider/model-id.` };
119
+ }
120
+ const model = params.registry.find(parsed.provider, parsed.modelId) as Model<Api> | undefined;
121
+ if (!model) {
122
+ return { kind: "failed", reason: `Classifier model ${params.modelSpec} is not available.` };
123
+ }
124
+ const label = boundReviewText(`${model.provider}/${model.id}`, 256);
125
+
126
+ const controller = new AbortController();
127
+ const abortFromCaller = () => controller.abort();
128
+ if (params.signal?.aborted) controller.abort();
129
+ else params.signal?.addEventListener("abort", abortFromCaller);
130
+ const timer = setTimeout(() => controller.abort(), params.timeoutMs);
131
+
132
+ try {
133
+ const response = await params.registry.complete(
134
+ model,
135
+ {
136
+ systemPrompt: CLASSIFIER_SYSTEM_PROMPT,
137
+ messages: [
138
+ {
139
+ role: "user",
140
+ content: [{ type: "text", text: params.prompt }],
141
+ timestamp: Date.now(),
142
+ },
143
+ ],
144
+ },
145
+ { signal: controller.signal, cacheRetention: "none" },
146
+ );
147
+ if (response.stopReason === "error" || response.stopReason === "aborted") {
148
+ return { kind: "failed", reason: response.errorMessage ?? `Classifier stopped: ${response.stopReason}` };
149
+ }
150
+ const text = response.content
151
+ .filter((block): block is { type: "text"; text: string } => block.type === "text")
152
+ .map((block) => block.text)
153
+ .join("")
154
+ .trim()
155
+ .toLowerCase();
156
+ if (/^high\b/.test(text)) return { kind: "high", model: label };
157
+ if (/^low\b/.test(text)) return { kind: "low", model: label };
158
+ return { kind: "failed", reason: `Classifier returned unreadable output: ${boundReviewText(text, 80)}` };
159
+ } catch (error) {
160
+ return { kind: "failed", reason: error instanceof Error ? error.message : String(error) };
161
+ } finally {
162
+ clearTimeout(timer);
163
+ params.signal?.removeEventListener("abort", abortFromCaller);
164
+ }
165
+ }