@benvargas/pi-openai-verbosity 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ben Vargas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # @benvargas/pi-openai-verbosity
2
+
3
+ Per-model text verbosity overrides for pi's `openai-codex` provider.
4
+
5
+ ## Why This Exists
6
+
7
+ This extension was originally created because pi sent OpenAI Codex provider requests with the default Responses
8
+ API text verbosity, which made some models, especially `gpt-5.5`, noticeably more verbose than the Codex CLI.
9
+ The goal was to align `openai-codex/gpt-5.5` with Codex CLI behavior by setting:
10
+
11
+ ```json
12
+ {
13
+ "text": {
14
+ "verbosity": "low"
15
+ }
16
+ }
17
+ ```
18
+
19
+ pi has since shipped an upstream fix: OpenAI Codex Responses requests now default to `low` verbosity when no
20
+ explicit verbosity is provided.
21
+
22
+ That upstream change addresses the original `gpt-5.5` issue, but it also means pi now defaults every
23
+ `openai-codex` model to `low`. That may not be ideal for all model slugs. For example, you may prefer `low` on
24
+ `gpt-5.5`, but `medium` on an older or different model such as `gpt-5.3-codex`.
25
+
26
+ This extension now provides the missing user-facing control: per-slug verbosity settings for pi's
27
+ `openai-codex` provider.
28
+
29
+ Requires pi `0.57.0` or newer.
30
+
31
+ ## What It Does
32
+
33
+ The extension uses pi's `before_provider_request` hook to rewrite outgoing provider payloads for configured
34
+ `openai-codex/<model>` keys.
35
+
36
+ For matching models, it sets:
37
+
38
+ ```json
39
+ {
40
+ "text": {
41
+ "verbosity": "low | medium | high"
42
+ }
43
+ }
44
+ ```
45
+
46
+ Non-matching models are left unchanged.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pi install npm:@benvargas/pi-openai-verbosity
52
+ ```
53
+
54
+ Or try without installing:
55
+
56
+ ```bash
57
+ pi -e npm:@benvargas/pi-openai-verbosity
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ Run pi with the extension enabled:
63
+
64
+ ```bash
65
+ pi -e npm:@benvargas/pi-openai-verbosity --model openai-codex/gpt-5.5
66
+ ```
67
+
68
+ Use `/openai-verbosity status` inside pi to report the configured rewrite for the current model. The command also
69
+ reloads the config file.
70
+
71
+ ## Config
72
+
73
+ Config files follow pi's project-over-global pattern:
74
+
75
+ - Project: `<repo>/.pi/extensions/pi-openai-verbosity.json`
76
+ - Global: `~/.pi/agent/extensions/pi-openai-verbosity.json`
77
+
78
+ If neither exists, the extension writes a default global config on first run.
79
+
80
+ Example config:
81
+
82
+ ```json
83
+ {
84
+ "models": {
85
+ "openai-codex/gpt-5.5": "low",
86
+ "openai-codex/gpt-5.4": "low",
87
+ "openai-codex/gpt-5.3-codex": "medium",
88
+ "openai-codex/gpt-5.3-codex-spark": "medium",
89
+ "openai-codex/gpt-5.2": "medium"
90
+ }
91
+ }
92
+ ```
93
+
94
+ Settings:
95
+
96
+ - `models`: object mapping `openai-codex/<model-id>` strings to `low`, `medium`, or `high`.
97
+
98
+ Project config overrides global config per model key. Any model not listed is left unchanged, which means pi's
99
+ native default behavior applies.
100
+
101
+ ## Default Config
102
+
103
+ By default, the extension preserves the original workaround behavior and sets known supported OpenAI Codex models
104
+ to `low`:
105
+
106
+ ```json
107
+ {
108
+ "models": {
109
+ "openai-codex/gpt-5.4": "low",
110
+ "openai-codex/gpt-5.5": "low",
111
+ "openai-codex/gpt-5.4-mini": "low",
112
+ "openai-codex/gpt-5.3-codex": "low",
113
+ "openai-codex/gpt-5.3-codex-spark": "low",
114
+ "openai-codex/gpt-5.2": "low",
115
+ "openai-codex/codex-auto-review": "low"
116
+ }
117
+ }
118
+ ```
119
+
120
+ You can change any value to `medium` or `high` to override pi's native low-verbosity default for that model.
121
+
122
+ ## Debugging
123
+
124
+ Pi does not currently expose a simple CLI flag to print the final provider request body. To verify this extension is
125
+ matching and rewriting a request, set `PI_OPENAI_VERBOSITY_DEBUG_LOG` to a JSONL file path.
126
+
127
+ | Variable | Description |
128
+ |---|---|
129
+ | `PI_OPENAI_VERBOSITY_DEBUG_LOG` | Set to a file path to enable debug logging. Matching requests write `"before"` and `"after"` JSON entries with the full provider payload. Non-matching requests write one `"skipped"` entry. |
130
+
131
+ ```bash
132
+ PI_OPENAI_VERBOSITY_DEBUG_LOG=/tmp/pi-openai-verbosity.jsonl \
133
+ pi -e npm:@benvargas/pi-openai-verbosity \
134
+ --model openai-codex/gpt-5.3-codex \
135
+ -p "Reply in one short sentence."
136
+ ```
137
+
138
+ Then inspect the last entries:
139
+
140
+ ```bash
141
+ tail -n 5 /tmp/pi-openai-verbosity.jsonl | jq .
142
+ ```
143
+
144
+ These entries include prompts, messages, tools, and the rest of the provider payload, so keep the file local and
145
+ delete it when you are done debugging.
146
+
147
+ ## Notes
148
+
149
+ - This extension only changes outgoing provider request payloads.
150
+ - Existing `text` fields are preserved, and only `text.verbosity` is replaced.
151
+ - Only the `openai-codex` provider is supported.
152
+ - This extension is most useful if you want different verbosity settings for different OpenAI Codex model slugs.
153
+
154
+ ## Uninstall
155
+
156
+ ```bash
157
+ pi remove npm:@benvargas/pi-openai-verbosity
158
+ ```
159
+
160
+ ## License
161
+
162
+ MIT
@@ -0,0 +1,323 @@
1
+ /**
2
+ * OpenAI verbosity for pi.
3
+ *
4
+ * Sets OpenAI Responses `text.verbosity` for configured models via the
5
+ * `before_provider_request` hook. Config precedence is project
6
+ * `.pi/extensions/pi-openai-verbosity.json` over global
7
+ * `~/.pi/agent/extensions/pi-openai-verbosity.json`.
8
+ */
9
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
13
+
14
+ const VERBOSITY_COMMAND = "openai-verbosity";
15
+ const VERBOSITY_CONFIG_BASENAME = "pi-openai-verbosity.json";
16
+ const VERBOSITY_COMMAND_ARGS = ["status"] as const;
17
+ const DEBUG_LOG_ENV = "PI_OPENAI_VERBOSITY_DEBUG_LOG";
18
+ const SUPPORTED_PROVIDERS = ["openai-codex"] as const;
19
+ const DEFAULT_MODEL_VERBOSITY = {
20
+ "openai-codex/gpt-5.4": "low",
21
+ "openai-codex/gpt-5.5": "low",
22
+ "openai-codex/gpt-5.4-mini": "low",
23
+ "openai-codex/gpt-5.3-codex": "low",
24
+ "openai-codex/gpt-5.3-codex-spark": "low",
25
+ "openai-codex/gpt-5.2": "low",
26
+ "openai-codex/codex-auto-review": "low",
27
+ } as const;
28
+
29
+ type TextVerbosity = "low" | "medium" | "high";
30
+
31
+ interface VerbosityConfigFile {
32
+ models?: Record<string, TextVerbosity>;
33
+ }
34
+
35
+ interface ResolvedVerbosityConfig {
36
+ configPath: string;
37
+ models: Record<string, TextVerbosity>;
38
+ }
39
+
40
+ type VerbosityPayload = {
41
+ text?: unknown;
42
+ [key: string]: unknown;
43
+ };
44
+
45
+ const DEFAULT_CONFIG_FILE: VerbosityConfigFile = {
46
+ models: { ...DEFAULT_MODEL_VERBOSITY },
47
+ };
48
+
49
+ function isRecord(value: unknown): value is Record<string, unknown> {
50
+ return typeof value === "object" && value !== null && !Array.isArray(value);
51
+ }
52
+
53
+ function isTextVerbosity(value: unknown): value is TextVerbosity {
54
+ return value === "low" || value === "medium" || value === "high";
55
+ }
56
+
57
+ function normalizeModelKey(value: string): string | undefined {
58
+ const trimmed = value.trim();
59
+ if (!trimmed) {
60
+ return undefined;
61
+ }
62
+ const slashIndex = trimmed.indexOf("/");
63
+ if (slashIndex <= 0 || slashIndex >= trimmed.length - 1) {
64
+ return undefined;
65
+ }
66
+ const provider = trimmed.slice(0, slashIndex).trim();
67
+ const id = trimmed.slice(slashIndex + 1).trim();
68
+ if (!provider || !id) {
69
+ return undefined;
70
+ }
71
+ if (!SUPPORTED_PROVIDERS.includes(provider as (typeof SUPPORTED_PROVIDERS)[number])) {
72
+ return undefined;
73
+ }
74
+ return `${provider}/${id}`;
75
+ }
76
+
77
+ function normalizeModelVerbosityMap(value: unknown): Record<string, TextVerbosity> | undefined {
78
+ if (value === undefined) {
79
+ return undefined;
80
+ }
81
+ if (!isRecord(value)) {
82
+ return undefined;
83
+ }
84
+
85
+ const normalized: Record<string, TextVerbosity> = {};
86
+ for (const [rawKey, rawVerbosity] of Object.entries(value)) {
87
+ const key = normalizeModelKey(rawKey);
88
+ if (!key || !isTextVerbosity(rawVerbosity)) {
89
+ continue;
90
+ }
91
+ normalized[key] = rawVerbosity;
92
+ }
93
+ return normalized;
94
+ }
95
+
96
+ function getConfigCwd(ctx: ExtensionContext): string {
97
+ return ctx.cwd || process.cwd();
98
+ }
99
+
100
+ function getConfigPaths(
101
+ cwd: string,
102
+ homeDir: string = homedir(),
103
+ ): {
104
+ projectConfigPath: string;
105
+ globalConfigPath: string;
106
+ } {
107
+ return {
108
+ projectConfigPath: join(cwd, ".pi", "extensions", VERBOSITY_CONFIG_BASENAME),
109
+ globalConfigPath: join(homeDir, ".pi", "agent", "extensions", VERBOSITY_CONFIG_BASENAME),
110
+ };
111
+ }
112
+
113
+ function readConfigFile(filePath: string): VerbosityConfigFile | null {
114
+ if (!existsSync(filePath)) {
115
+ return null;
116
+ }
117
+ try {
118
+ const raw = readFileSync(filePath, "utf-8");
119
+ const parsed = JSON.parse(raw) as unknown;
120
+ if (!isRecord(parsed)) {
121
+ return {};
122
+ }
123
+ const models = normalizeModelVerbosityMap(parsed.models);
124
+ return models === undefined ? {} : { models };
125
+ } catch (error) {
126
+ const message = error instanceof Error ? error.message : String(error);
127
+ console.warn(`[pi-openai-verbosity] Failed to read ${filePath}: ${message}`);
128
+ return null;
129
+ }
130
+ }
131
+
132
+ function writeConfigFile(filePath: string, config: VerbosityConfigFile): void {
133
+ try {
134
+ mkdirSync(dirname(filePath), { recursive: true });
135
+ writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
136
+ } catch (error) {
137
+ const message = error instanceof Error ? error.message : String(error);
138
+ console.warn(`[pi-openai-verbosity] Failed to write ${filePath}: ${message}`);
139
+ }
140
+ }
141
+
142
+ function ensureDefaultConfigFile(projectConfigPath: string, globalConfigPath: string): void {
143
+ if (existsSync(projectConfigPath) || existsSync(globalConfigPath)) {
144
+ return;
145
+ }
146
+ writeConfigFile(globalConfigPath, DEFAULT_CONFIG_FILE);
147
+ }
148
+
149
+ function resolveVerbosityConfig(cwd: string, homeDir: string = homedir()): ResolvedVerbosityConfig {
150
+ const { projectConfigPath, globalConfigPath } = getConfigPaths(cwd, homeDir);
151
+ ensureDefaultConfigFile(projectConfigPath, globalConfigPath);
152
+
153
+ const globalConfig = readConfigFile(globalConfigPath) ?? {};
154
+ const projectConfig = readConfigFile(projectConfigPath) ?? {};
155
+ const selectedConfigPath = existsSync(projectConfigPath) ? projectConfigPath : globalConfigPath;
156
+
157
+ return {
158
+ configPath: selectedConfigPath,
159
+ models: {
160
+ ...DEFAULT_MODEL_VERBOSITY,
161
+ ...(globalConfig.models ?? {}),
162
+ ...(projectConfig.models ?? {}),
163
+ },
164
+ };
165
+ }
166
+
167
+ function getCurrentModelKey(model: ExtensionContext["model"]): string | undefined {
168
+ if (!model) {
169
+ return undefined;
170
+ }
171
+ return `${model.provider}/${model.id}`;
172
+ }
173
+
174
+ function getVerbosityForModel(
175
+ model: ExtensionContext["model"],
176
+ models: Record<string, TextVerbosity>,
177
+ ): TextVerbosity | undefined {
178
+ const modelKey = getCurrentModelKey(model);
179
+ return modelKey ? models[modelKey] : undefined;
180
+ }
181
+
182
+ function describeConfiguredModels(models: Record<string, TextVerbosity>): string {
183
+ const entries = Object.entries(models);
184
+ if (entries.length === 0) {
185
+ return "none configured";
186
+ }
187
+ return entries.map(([model, verbosity]) => `${model}=${verbosity}`).join(", ");
188
+ }
189
+
190
+ function describeCurrentState(ctx: ExtensionContext, config: ResolvedVerbosityConfig): string {
191
+ const model = getCurrentModelKey(ctx.model) ?? "none";
192
+ const verbosity = getVerbosityForModel(ctx.model, config.models);
193
+ if (verbosity) {
194
+ return `OpenAI verbosity sets text.verbosity=${verbosity} for ${model}.`;
195
+ }
196
+ return `OpenAI verbosity has no setting configured for ${model}. Configured models: ${describeConfiguredModels(
197
+ config.models,
198
+ )}.`;
199
+ }
200
+
201
+ function applyTextVerbosity(payload: unknown, verbosity: TextVerbosity): unknown {
202
+ if (!isRecord(payload)) {
203
+ return payload;
204
+ }
205
+
206
+ const nextPayload: VerbosityPayload = { ...payload };
207
+ const text = isRecord(nextPayload.text) ? { ...nextPayload.text } : {};
208
+ text.verbosity = verbosity;
209
+ nextPayload.text = text;
210
+ return nextPayload;
211
+ }
212
+
213
+ function getPayloadTextVerbosity(payload: unknown): unknown {
214
+ if (!isRecord(payload) || !isRecord(payload.text)) {
215
+ return undefined;
216
+ }
217
+ return payload.text.verbosity;
218
+ }
219
+
220
+ function writeDebugLog(
221
+ entry: Record<string, unknown>,
222
+ debugLogPath: string | undefined = process.env[DEBUG_LOG_ENV],
223
+ ): void {
224
+ if (!debugLogPath) {
225
+ return;
226
+ }
227
+ try {
228
+ mkdirSync(dirname(debugLogPath), { recursive: true });
229
+ appendFileSync(debugLogPath, `${JSON.stringify({ timestamp: new Date().toISOString(), ...entry })}\n`, "utf-8");
230
+ } catch (error) {
231
+ const message = error instanceof Error ? error.message : String(error);
232
+ console.warn(`[pi-openai-verbosity] Failed to write debug log ${debugLogPath}: ${message}`);
233
+ }
234
+ }
235
+
236
+ export default function piOpenAIVerbosity(pi: ExtensionAPI): void {
237
+ let cachedConfig: ResolvedVerbosityConfig | undefined;
238
+
239
+ function refreshConfig(ctx: ExtensionContext): ResolvedVerbosityConfig {
240
+ cachedConfig = resolveVerbosityConfig(getConfigCwd(ctx));
241
+ return cachedConfig;
242
+ }
243
+
244
+ function getConfig(ctx: ExtensionContext): ResolvedVerbosityConfig {
245
+ return cachedConfig ?? refreshConfig(ctx);
246
+ }
247
+
248
+ pi.registerCommand(VERBOSITY_COMMAND, {
249
+ description: "Report configured GPT text verbosity rewrites",
250
+ getArgumentCompletions: (prefix) => {
251
+ const items = VERBOSITY_COMMAND_ARGS.filter((value) => value.startsWith(prefix)).map((value) => ({
252
+ value,
253
+ label: value,
254
+ }));
255
+ return items.length > 0 ? items : null;
256
+ },
257
+ handler: async (args, ctx) => {
258
+ const command = args.trim().toLowerCase();
259
+ if (command.length > 0 && command !== "status") {
260
+ ctx.ui.notify("Usage: /openai-verbosity [status]", "error");
261
+ return;
262
+ }
263
+ ctx.ui.notify(describeCurrentState(ctx, refreshConfig(ctx)), "info");
264
+ },
265
+ });
266
+
267
+ pi.on("before_provider_request", (event, ctx) => {
268
+ const model = getCurrentModelKey(ctx.model) ?? null;
269
+ const beforeTextVerbosity = getPayloadTextVerbosity(event.payload);
270
+ const verbosity = getVerbosityForModel(ctx.model, getConfig(ctx).models);
271
+ if (!verbosity) {
272
+ writeDebugLog({
273
+ stage: "skipped",
274
+ model,
275
+ matched: false,
276
+ beforeTextVerbosity: beforeTextVerbosity ?? null,
277
+ payload: event.payload,
278
+ });
279
+ return;
280
+ }
281
+ writeDebugLog({
282
+ stage: "before",
283
+ model,
284
+ matched: true,
285
+ configuredVerbosity: verbosity,
286
+ beforeTextVerbosity: beforeTextVerbosity ?? null,
287
+ payload: event.payload,
288
+ });
289
+ const nextPayload = applyTextVerbosity(event.payload, verbosity);
290
+ writeDebugLog({
291
+ stage: "after",
292
+ model,
293
+ matched: true,
294
+ configuredVerbosity: verbosity,
295
+ beforeTextVerbosity: beforeTextVerbosity ?? null,
296
+ afterTextVerbosity: getPayloadTextVerbosity(nextPayload) ?? null,
297
+ payload: nextPayload,
298
+ });
299
+ return nextPayload;
300
+ });
301
+ }
302
+
303
+ export const _test = {
304
+ VERBOSITY_COMMAND,
305
+ VERBOSITY_CONFIG_BASENAME,
306
+ VERBOSITY_COMMAND_ARGS,
307
+ SUPPORTED_PROVIDERS,
308
+ DEFAULT_MODEL_VERBOSITY,
309
+ DEFAULT_CONFIG_FILE,
310
+ isTextVerbosity,
311
+ normalizeModelKey,
312
+ normalizeModelVerbosityMap,
313
+ getConfigPaths,
314
+ readConfigFile,
315
+ resolveVerbosityConfig,
316
+ getCurrentModelKey,
317
+ getVerbosityForModel,
318
+ describeConfiguredModels,
319
+ describeCurrentState,
320
+ applyTextVerbosity,
321
+ getPayloadTextVerbosity,
322
+ writeDebugLog,
323
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@benvargas/pi-openai-verbosity",
3
+ "version": "1.0.0",
4
+ "description": "Per-model OpenAI Codex text verbosity overrides for pi",
5
+ "keywords": [
6
+ "pi",
7
+ "pi-package",
8
+ "pi-extension",
9
+ "pi-coding-agent",
10
+ "openai",
11
+ "codex",
12
+ "openai-codex",
13
+ "gpt-5.5",
14
+ "gpt-5.3-codex",
15
+ "verbosity",
16
+ "text-verbosity",
17
+ "per-model"
18
+ ],
19
+ "type": "module",
20
+ "files": [
21
+ "extensions/",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "pi": {
26
+ "extensions": [
27
+ "./extensions/index.ts"
28
+ ]
29
+ },
30
+ "peerDependencies": {
31
+ "@mariozechner/pi-coding-agent": ">=0.57.0"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/ben-vargas/pi-packages.git",
36
+ "directory": "packages/pi-openai-verbosity"
37
+ },
38
+ "author": "Ben Vargas",
39
+ "license": "MIT",
40
+ "bugs": {
41
+ "url": "https://github.com/ben-vargas/pi-packages/issues"
42
+ },
43
+ "homepage": "https://github.com/ben-vargas/pi-packages/tree/main/packages/pi-openai-verbosity#readme"
44
+ }