pi-openai-toolkit 0.14.4 → 0.14.6

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.
Files changed (43) hide show
  1. package/README.md +119 -147
  2. package/README.zh.md +119 -151
  3. package/docs/internals.md +42 -21
  4. package/package.json +127 -127
  5. package/src/auto-mode/classifier.ts +165 -165
  6. package/src/auto-mode/policy.ts +146 -146
  7. package/src/auto-mode/prompt.ts +279 -279
  8. package/src/auto-mode/reviewer.ts +325 -325
  9. package/src/auto-mode/transcript.ts +133 -133
  10. package/src/auto-mode/types.ts +137 -137
  11. package/src/codex-astra/effort-planner.ts +175 -175
  12. package/src/codex-astra/extension.ts +122 -122
  13. package/src/config.ts +621 -592
  14. package/src/context-management/codex-provider.ts +260 -260
  15. package/src/context-management/history-notes.ts +357 -325
  16. package/src/context-management/messages.ts +112 -106
  17. package/src/context-management/namespace-tools.ts +426 -426
  18. package/src/context-management/tool-contract.ts +23 -23
  19. package/src/context-management/tools.ts +306 -311
  20. package/src/context-management/types.ts +168 -161
  21. package/src/context-management/window-budget.ts +83 -83
  22. package/src/context-management/window-manager.ts +60 -51
  23. package/src/context-management/window-request.ts +125 -125
  24. package/src/debug.ts +1 -1
  25. package/src/extension-runtime.ts +34 -9
  26. package/src/image-generation/artifacts.ts +237 -237
  27. package/src/image-generation/client.ts +248 -248
  28. package/src/image-generation/eligibility.ts +18 -18
  29. package/src/image-generation/extension.ts +19 -2
  30. package/src/image-generation/protocol.ts +473 -405
  31. package/src/image-generation/references.ts +194 -194
  32. package/src/image-generation/render.ts +252 -252
  33. package/src/image-generation/service.ts +249 -243
  34. package/src/image-generation/types.ts +189 -184
  35. package/src/model-scope.ts +17 -17
  36. package/src/provider-headers.ts +23 -23
  37. package/src/responses-headers.ts +173 -108
  38. package/src/runtime.ts +513 -477
  39. package/src/types.ts +11 -0
  40. package/src/web-search/extension.ts +80 -80
  41. package/src/web-search/payload.ts +96 -96
  42. package/src/web-search/prompt.ts +20 -20
  43. package/src/web-search/types.ts +45 -45
@@ -1,175 +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
- }
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
+ }
@@ -1,122 +1,122 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { ASTRA_MODEL_ID } from "../types";
3
- import { CODEX_CLIENT_VERSION } from "../responses-headers";
4
- import {
5
- getEffortControlState,
6
- planStableEffort,
7
- type EffortControlState,
8
- type PlannableInputItem,
9
- } from "./effort-planner";
10
-
11
- type AstraPayload = {
12
- model: string;
13
- input: PlannableInputItem[];
14
- reasoning: { effort: string; [key: string]: unknown };
15
- [key: string]: unknown;
16
- };
17
-
18
- function isRecord(value: unknown): value is Record<string, unknown> {
19
- return !!value && typeof value === "object" && !Array.isArray(value);
20
- }
21
-
22
- /**
23
- * Recognize the live Codex Responses request body: model + wire input array
24
- * + a request-level string effort. Anything else (titles, embeddings, or a
25
- * shape we do not understand) is left untouched.
26
- */
27
- function parseAstraPayload(payload: unknown): AstraPayload | undefined {
28
- if (!isRecord(payload)) return undefined;
29
- if (typeof payload.model !== "string") return undefined;
30
- if (!Array.isArray(payload.input)) return undefined;
31
- const reasoning = payload.reasoning;
32
- if (!isRecord(reasoning) || typeof reasoning.effort !== "string") return undefined;
33
- if (reasoning.effort.length === 0 || reasoning.effort === "none") return undefined;
34
- if (!payload.input.every((item) => isRecord(item))) return undefined;
35
- return payload as unknown as AstraPayload;
36
- }
37
-
38
- function isCompactionShaped(payload: AstraPayload): boolean {
39
- // A `compaction_trigger` history belongs to a compact endpoint request,
40
- // which rejects `configuration_update` items. Our compaction clients use
41
- // direct fetch and never reach this hook; this guard covers any future
42
- // provider-routed compaction.
43
- const last = payload.input[payload.input.length - 1];
44
- return !!last && isRecord(last) && last.type === "compaction_trigger";
45
- }
46
-
47
- function getSessionId(ctx: { sessionManager: { getSessionId(): string } }): string | undefined {
48
- try {
49
- return ctx.sessionManager.getSessionId();
50
- } catch {
51
- return undefined;
52
- }
53
- }
54
-
55
- export function registerCodexAstraExtension(pi: ExtensionAPI): void {
56
- const states = new Map<string, EffortControlState>();
57
-
58
- // Switching or forking a session rebuilds the runtime and re-fires
59
- // session_start; clearing there covers every replacement path.
60
- pi.on("session_start", () => {
61
- states.clear();
62
- });
63
-
64
- pi.on("before_provider_request", (event, ctx) => {
65
- try {
66
- // Silent activation by design: the Astra compatibility layer exists only
67
- // to keep `gpt-6-astra` working through the Codex backend gate, so it
68
- // keys off the bare model id instead of an operator allowlist.
69
- const model = ctx.model;
70
- // Both Responses wire families carry the same reasoning.effort prefix
71
- // problem, and upstream OMP plans on both providers.
72
- if (!model || (model.api !== "openai-codex-responses" && model.api !== "openai-responses")) {
73
- return undefined;
74
- }
75
- if (model.id !== ASTRA_MODEL_ID) return undefined;
76
-
77
- const payload = parseAstraPayload(event.payload);
78
- // The payload must belong to the model the eligibility check just
79
- // passed; a mismatch means this request is not ours to plan.
80
- if (!payload || payload.model !== model.id) return undefined;
81
- if (isCompactionShaped(payload)) return undefined;
82
-
83
- const sessionId = getSessionId(ctx);
84
- const state = getEffortControlState(
85
- states,
86
- `${model.baseUrl ?? ""}\u0000${model.id}\u0000${sessionId ?? "no-session"}`,
87
- );
88
-
89
- // Idempotence: never plan on top of items a previous pass injected.
90
- const history = payload.input.filter((item) => item.type !== "configuration_update");
91
- const { input, result } = planStableEffort(state, history, payload.reasoning.effort);
92
- if (result.effort === payload.reasoning.effort && result.spliced === 0) return undefined;
93
-
94
- return {
95
- ...payload,
96
- input,
97
- reasoning: { ...payload.reasoning, effort: result.effort },
98
- };
99
- } catch {
100
- // A planner failure must never break the provider request path.
101
- return undefined;
102
- }
103
- });
104
-
105
- pi.on("before_provider_headers", (event, ctx) => {
106
- try {
107
- const model = ctx.model;
108
- if (!model || model.api !== "openai-codex-responses") return;
109
- // Live requests need the same backend version gate as our synthetic
110
- // ones: an old or missing `version` never reaches gated SKUs.
111
- if (!event.headers.version) {
112
- event.headers.version = CODEX_CLIENT_VERSION;
113
- }
114
- } catch {
115
- // Header injection is best-effort.
116
- }
117
- });
118
- }
119
-
120
- export default function codexAstraExtension(pi: ExtensionAPI): void {
121
- registerCodexAstraExtension(pi);
122
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { ASTRA_MODEL_ID } from "../types";
3
+ import { CODEX_CLIENT_VERSION } from "../responses-headers";
4
+ import {
5
+ getEffortControlState,
6
+ planStableEffort,
7
+ type EffortControlState,
8
+ type PlannableInputItem,
9
+ } from "./effort-planner";
10
+
11
+ type AstraPayload = {
12
+ model: string;
13
+ input: PlannableInputItem[];
14
+ reasoning: { effort: string; [key: string]: unknown };
15
+ [key: string]: unknown;
16
+ };
17
+
18
+ function isRecord(value: unknown): value is Record<string, unknown> {
19
+ return !!value && typeof value === "object" && !Array.isArray(value);
20
+ }
21
+
22
+ /**
23
+ * Recognize the live Codex Responses request body: model + wire input array
24
+ * + a request-level string effort. Anything else (titles, embeddings, or a
25
+ * shape we do not understand) is left untouched.
26
+ */
27
+ function parseAstraPayload(payload: unknown): AstraPayload | undefined {
28
+ if (!isRecord(payload)) return undefined;
29
+ if (typeof payload.model !== "string") return undefined;
30
+ if (!Array.isArray(payload.input)) return undefined;
31
+ const reasoning = payload.reasoning;
32
+ if (!isRecord(reasoning) || typeof reasoning.effort !== "string") return undefined;
33
+ if (reasoning.effort.length === 0 || reasoning.effort === "none") return undefined;
34
+ if (!payload.input.every((item) => isRecord(item))) return undefined;
35
+ return payload as unknown as AstraPayload;
36
+ }
37
+
38
+ function isCompactionShaped(payload: AstraPayload): boolean {
39
+ // A `compaction_trigger` history belongs to a compact endpoint request,
40
+ // which rejects `configuration_update` items. Our compaction clients use
41
+ // direct fetch and never reach this hook; this guard covers any future
42
+ // provider-routed compaction.
43
+ const last = payload.input[payload.input.length - 1];
44
+ return !!last && isRecord(last) && last.type === "compaction_trigger";
45
+ }
46
+
47
+ function getSessionId(ctx: { sessionManager: { getSessionId(): string } }): string | undefined {
48
+ try {
49
+ return ctx.sessionManager.getSessionId();
50
+ } catch {
51
+ return undefined;
52
+ }
53
+ }
54
+
55
+ export function registerCodexAstraExtension(pi: ExtensionAPI): void {
56
+ const states = new Map<string, EffortControlState>();
57
+
58
+ // Switching or forking a session rebuilds the runtime and re-fires
59
+ // session_start; clearing there covers every replacement path.
60
+ pi.on("session_start", () => {
61
+ states.clear();
62
+ });
63
+
64
+ pi.on("before_provider_request", (event, ctx) => {
65
+ try {
66
+ // Silent activation by design: the Astra compatibility layer exists only
67
+ // to keep `gpt-6-astra` working through the Codex backend gate, so it
68
+ // keys off the bare model id instead of an operator allowlist.
69
+ const model = ctx.model;
70
+ // Both Responses wire families carry the same reasoning.effort prefix
71
+ // problem, and upstream OMP plans on both providers.
72
+ if (!model || (model.api !== "openai-codex-responses" && model.api !== "openai-responses")) {
73
+ return undefined;
74
+ }
75
+ if (model.id !== ASTRA_MODEL_ID) return undefined;
76
+
77
+ const payload = parseAstraPayload(event.payload);
78
+ // The payload must belong to the model the eligibility check just
79
+ // passed; a mismatch means this request is not ours to plan.
80
+ if (!payload || payload.model !== model.id) return undefined;
81
+ if (isCompactionShaped(payload)) return undefined;
82
+
83
+ const sessionId = getSessionId(ctx);
84
+ const state = getEffortControlState(
85
+ states,
86
+ `${model.baseUrl ?? ""}\u0000${model.id}\u0000${sessionId ?? "no-session"}`,
87
+ );
88
+
89
+ // Idempotence: never plan on top of items a previous pass injected.
90
+ const history = payload.input.filter((item) => item.type !== "configuration_update");
91
+ const { input, result } = planStableEffort(state, history, payload.reasoning.effort);
92
+ if (result.effort === payload.reasoning.effort && result.spliced === 0) return undefined;
93
+
94
+ return {
95
+ ...payload,
96
+ input,
97
+ reasoning: { ...payload.reasoning, effort: result.effort },
98
+ };
99
+ } catch {
100
+ // A planner failure must never break the provider request path.
101
+ return undefined;
102
+ }
103
+ });
104
+
105
+ pi.on("before_provider_headers", (event, ctx) => {
106
+ try {
107
+ const model = ctx.model;
108
+ if (!model || model.api !== "openai-codex-responses") return;
109
+ // Live requests need the same backend version gate as our synthetic
110
+ // ones: an old or missing `version` never reaches gated SKUs.
111
+ if (!event.headers.version) {
112
+ event.headers.version = CODEX_CLIENT_VERSION;
113
+ }
114
+ } catch {
115
+ // Header injection is best-effort.
116
+ }
117
+ });
118
+ }
119
+
120
+ export default function codexAstraExtension(pi: ExtensionAPI): void {
121
+ registerCodexAstraExtension(pi);
122
+ }