@monotykamary/pi-better-openai 0.1.39 → 0.1.41
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/README.md +3 -2
- package/index.ts +35 -0
- package/package.json +2 -2
- package/src/codex-auth.ts +26 -3
- package/src/config.ts +5 -3
- package/src/multiprovider.ts +66 -0
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ Usage display, image generation, and live voice require pi's `openai-codex` OAut
|
|
|
26
26
|
2. Verify subscription usage with `/openai-usage`, or open `/openai-settings` and check **Diagnostics**.
|
|
27
27
|
3. The extension reads auth from pi's agent auth store, normally `~/.pi/agent/auth.json`. Do not copy, paste, or commit values from this file.
|
|
28
28
|
4. If `PI_CODING_AGENT_DIR` is set, the auth store, global extension config, and global generated-image directory use that agent directory instead of `~/.pi/agent`. A leading `~/` is expanded to your home directory.
|
|
29
|
+
5. When [pi-multiprovider](https://github.com/monotykamary/pi-multiprovider) 0.4.0+ pools several `openai-codex` accounts, the session's active account (chosen with `/switch-account`) is resolved first for usage display, image generation, web search, and live voice; the usage widget refreshes on every switch. Without that extension, credential resolution is unchanged.
|
|
29
30
|
|
|
30
31
|
## Features
|
|
31
32
|
|
|
@@ -89,7 +90,7 @@ Example config:
|
|
|
89
90
|
},
|
|
90
91
|
"image": {
|
|
91
92
|
"enabled": true,
|
|
92
|
-
"defaultModel": "gpt-image-2",
|
|
93
|
+
"defaultModel": "gpt-image-2.5",
|
|
93
94
|
"defaultSave": "project",
|
|
94
95
|
"outputFormat": "png",
|
|
95
96
|
"timeoutMs": 180000
|
|
@@ -147,7 +148,7 @@ Agents can call the `openai_image` tool directly. Supported parameters:
|
|
|
147
148
|
- `prompt` (required): pass the user's image wording verbatim.
|
|
148
149
|
- `action`: `auto`, `generate`, or `edit`. `auto` uses the edit endpoint when `images` are supplied; explicit `edit` requires images, while explicit `generate` does not accept them.
|
|
149
150
|
- `images`: up to five distinct project-local reference/edit image paths. Paths must stay inside the current workspace and point to readable PNG, JPEG, WebP, or GIF files; each file is limited to 20 MB and the combined input to 50 MB.
|
|
150
|
-
- `model`: GPT Image model override for the standalone Codex Images API, for example `gpt-image-2`.
|
|
151
|
+
- `model`: GPT Image model override for the standalone Codex Images API, for example `gpt-image-2.5` or `gpt-image-2`. Values outside the `gpt-image-` family, including legacy Responses chat models, are replaced with the configured default image model.
|
|
151
152
|
- `outputFormat`: `png`, `jpeg`, or `webp`. Codex returns PNG and the extension converts other formats locally.
|
|
152
153
|
- `save`: `project`, `global`, `custom`, or `none`.
|
|
153
154
|
- `saveDir`: required for `save: "custom"` unless `PI_IMAGE_SAVE_DIR` is set.
|
package/index.ts
CHANGED
|
@@ -14,6 +14,13 @@ import {
|
|
|
14
14
|
} from "@earendil-works/pi-tui";
|
|
15
15
|
import { registerOpenAICodexModels } from "./src/codex-models.ts";
|
|
16
16
|
import { CONFIG_BASENAME, STATUS_KEY } from "./src/identity.ts";
|
|
17
|
+
import {
|
|
18
|
+
CODEX_PROVIDER_ID,
|
|
19
|
+
isMultiproviderService,
|
|
20
|
+
MULTIPROVIDER_SERVICE_EVENT,
|
|
21
|
+
setActiveMultiproviderService,
|
|
22
|
+
type MultiproviderService,
|
|
23
|
+
} from "./src/multiprovider.ts";
|
|
17
24
|
import {
|
|
18
25
|
formatTokens,
|
|
19
26
|
redactDiagnosticValue,
|
|
@@ -227,6 +234,32 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
227
234
|
let cachedSessionName: string | undefined;
|
|
228
235
|
const usageController = new UsageController(config, updateFooter);
|
|
229
236
|
const petController = new PetFooterController(config, updateFooter, () => footerInstalled);
|
|
237
|
+
let multiproviderService: MultiproviderService | undefined;
|
|
238
|
+
let unsubscribeMultiprovider: (() => void) | undefined;
|
|
239
|
+
let multiproviderRefreshCtx: ExtensionContext | undefined;
|
|
240
|
+
|
|
241
|
+
// Follow pi-multiprovider's active pooled account for openai-codex. The
|
|
242
|
+
// event re-fires with the same stable object at load and session start; the
|
|
243
|
+
// identity check keeps the change subscription attached exactly once. When
|
|
244
|
+
// the extension is absent, nothing here activates and credential resolution
|
|
245
|
+
// keeps its standalone behavior.
|
|
246
|
+
if (typeof pi.events?.on === "function") {
|
|
247
|
+
pi.events.on(MULTIPROVIDER_SERVICE_EVENT, (value) => {
|
|
248
|
+
if (!isMultiproviderService(value) || value === multiproviderService) return;
|
|
249
|
+
unsubscribeMultiprovider?.();
|
|
250
|
+
multiproviderService = value;
|
|
251
|
+
setActiveMultiproviderService(value);
|
|
252
|
+
unsubscribeMultiprovider = value.onActiveAccountChanged(CODEX_PROVIDER_ID, (event) => {
|
|
253
|
+
void usageController.refresh(event.ctx, undefined, { force: true });
|
|
254
|
+
updateFooter(event.ctx);
|
|
255
|
+
});
|
|
256
|
+
const ctx = multiproviderRefreshCtx;
|
|
257
|
+
if (ctx) {
|
|
258
|
+
void usageController.refresh(ctx, undefined, { force: true });
|
|
259
|
+
updateFooter(ctx);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
230
263
|
|
|
231
264
|
function refresh(ctx: ExtensionContext): ResolvedConfig {
|
|
232
265
|
cachedConfig = resolveConfig(ctx.cwd || process.cwd());
|
|
@@ -1261,6 +1294,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1261
1294
|
pi.on("session_start", (_event, ctx) => {
|
|
1262
1295
|
invalidateContextUsage();
|
|
1263
1296
|
invalidateSessionName();
|
|
1297
|
+
multiproviderRefreshCtx = ctx;
|
|
1264
1298
|
const nextConfig = refresh(ctx);
|
|
1265
1299
|
fastController.initializeForSession(ctx, nextConfig, pi.getFlag(FLAG) === true);
|
|
1266
1300
|
if (
|
|
@@ -1347,6 +1381,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1347
1381
|
pi.on("session_shutdown", () => {
|
|
1348
1382
|
invalidateContextUsage();
|
|
1349
1383
|
invalidateSessionName();
|
|
1384
|
+
multiproviderRefreshCtx = undefined;
|
|
1350
1385
|
usageController.shutdown();
|
|
1351
1386
|
petController.shutdown();
|
|
1352
1387
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monotykamary/pi-better-openai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
4
4
|
"description": "Improve OpenAI in pi with fast mode, usage stats, realtime voice, image generation, and footer polish.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fast",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"https-proxy-agent": "9.1.0",
|
|
59
59
|
"proxy-from-env": "2.1.0",
|
|
60
|
-
"sharp": "0.35.
|
|
60
|
+
"sharp": "0.35.4",
|
|
61
61
|
"undici": "8.10.0",
|
|
62
62
|
"ws": "8.21.2"
|
|
63
63
|
},
|
package/src/codex-auth.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { CODEX_PROVIDER_ID, getActiveMultiproviderService } from "./multiprovider.ts";
|
|
4
5
|
import { piAgentDir } from "./paths.ts";
|
|
5
6
|
|
|
6
7
|
export const AUTH_FILE = join(piAgentDir(), "auth.json");
|
|
@@ -10,8 +11,13 @@ export type CodexCredentials = {
|
|
|
10
11
|
accountId: string;
|
|
11
12
|
};
|
|
12
13
|
|
|
14
|
+
export type CodexCredentialsContext = Pick<
|
|
15
|
+
ExtensionContext,
|
|
16
|
+
"modelRegistry" | "model" | "sessionManager"
|
|
17
|
+
>;
|
|
18
|
+
|
|
13
19
|
export type CodexCredentialsWithSource = CodexCredentials & {
|
|
14
|
-
source: "modelRegistry" | "authFile";
|
|
20
|
+
source: "multiprovider" | "modelRegistry" | "authFile";
|
|
15
21
|
};
|
|
16
22
|
|
|
17
23
|
function waitForSignal<T>(operation: Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
@@ -118,11 +124,28 @@ export function readCodexAuth(): CodexCredentials | undefined {
|
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
export async function getCodexCredentials(
|
|
121
|
-
ctx?:
|
|
127
|
+
ctx?: CodexCredentialsContext,
|
|
122
128
|
signal?: AbortSignal,
|
|
123
129
|
): Promise<CodexCredentialsWithSource | undefined> {
|
|
124
130
|
if (signal?.aborted) throw signal.reason ?? new Error("Operation was aborted.");
|
|
125
|
-
|
|
131
|
+
// A pooled account pinned for this session (pi-multiprovider /switch-account)
|
|
132
|
+
// wins over pi's own credential: subscription usage is per-account.
|
|
133
|
+
const multiprovider = getActiveMultiproviderService();
|
|
134
|
+
if (multiprovider && ctx) {
|
|
135
|
+
try {
|
|
136
|
+
const resolved = await waitForSignal(
|
|
137
|
+
multiprovider.resolveActiveAccountAuth(CODEX_PROVIDER_ID, ctx, signal),
|
|
138
|
+
signal,
|
|
139
|
+
);
|
|
140
|
+
const accountId = resolved ? extractAccountIdFromJwt(resolved.accessToken) : undefined;
|
|
141
|
+
if (resolved && accountId) {
|
|
142
|
+
return { accessToken: resolved.accessToken, accountId, source: "multiprovider" };
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// Fall back to pi-owned credential resolution.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const registryRequest = ctx?.modelRegistry?.getApiKeyForProvider(CODEX_PROVIDER_ID);
|
|
126
149
|
const registryToken = registryRequest
|
|
127
150
|
? await waitForSignal(
|
|
128
151
|
registryRequest.catch(() => undefined),
|
package/src/config.ts
CHANGED
|
@@ -146,7 +146,9 @@ export const DEFAULT_FOOTER_CONFIG: Required<FooterConfig> = {
|
|
|
146
146
|
mode: "replace",
|
|
147
147
|
};
|
|
148
148
|
|
|
149
|
-
export const
|
|
149
|
+
export const IMAGE_MODEL_CHOICES = ["gpt-image-2.5", "gpt-image-2"] as const;
|
|
150
|
+
|
|
151
|
+
export const DEFAULT_IMAGE_MODEL = "gpt-image-2.5";
|
|
150
152
|
|
|
151
153
|
export const DEFAULT_IMAGE_CONFIG: Required<ImageConfig> = {
|
|
152
154
|
enabled: true,
|
|
@@ -313,7 +315,7 @@ export const IMAGE_SETTING_DESCRIPTORS: readonly SettingsOptionDescriptor[] = [
|
|
|
313
315
|
key: "defaultModel",
|
|
314
316
|
label: "Image model",
|
|
315
317
|
currentValue: (cfg) => cfg.image.defaultModel,
|
|
316
|
-
values:
|
|
318
|
+
values: IMAGE_MODEL_CHOICES,
|
|
317
319
|
description: "GPT Image model used by the standalone Codex Images API.",
|
|
318
320
|
parse: stringSetting,
|
|
319
321
|
},
|
|
@@ -606,7 +608,7 @@ export function readRawConfig(path: string): Record<string, unknown> {
|
|
|
606
608
|
export function normalizeImageModel(value: string): string {
|
|
607
609
|
const trimmed = value.trim();
|
|
608
610
|
const model = trimmed.includes("/") ? trimmed.split("/").pop() || trimmed : trimmed;
|
|
609
|
-
return model.startsWith("gpt-
|
|
611
|
+
return model.startsWith("gpt-image-") ? model : DEFAULT_IMAGE_MODEL;
|
|
610
612
|
}
|
|
611
613
|
|
|
612
614
|
export function readConfig(path: string): ConfigFile | undefined {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
// Soft bridge to pi-multiprovider. When that extension is installed it emits
|
|
4
|
+
// this event with a service object; without it everything here stays inert and
|
|
5
|
+
// credential resolution keeps its standalone behavior.
|
|
6
|
+
export const MULTIPROVIDER_SERVICE_EVENT = "pi-multiprovider:service";
|
|
7
|
+
export const CODEX_PROVIDER_ID = "openai-codex";
|
|
8
|
+
|
|
9
|
+
export type MultiproviderActiveAccount = {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
authKind: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type MultiproviderAccountAuth = {
|
|
16
|
+
accessToken: string;
|
|
17
|
+
label: string;
|
|
18
|
+
source?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type MultiproviderAccountChangedEvent = {
|
|
22
|
+
providerId: string;
|
|
23
|
+
account: MultiproviderActiveAccount | undefined;
|
|
24
|
+
ctx: ExtensionContext;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type MultiproviderServiceContext = Pick<
|
|
28
|
+
ExtensionContext,
|
|
29
|
+
"modelRegistry" | "model" | "sessionManager"
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
export type MultiproviderService = {
|
|
33
|
+
getActiveAccount(
|
|
34
|
+
providerId: string,
|
|
35
|
+
ctx: MultiproviderServiceContext,
|
|
36
|
+
): Promise<MultiproviderActiveAccount | undefined>;
|
|
37
|
+
resolveActiveAccountAuth(
|
|
38
|
+
providerId: string,
|
|
39
|
+
ctx: MultiproviderServiceContext,
|
|
40
|
+
signal?: AbortSignal,
|
|
41
|
+
): Promise<MultiproviderAccountAuth | undefined>;
|
|
42
|
+
onActiveAccountChanged(
|
|
43
|
+
providerId: string,
|
|
44
|
+
callback: (event: MultiproviderAccountChangedEvent) => void,
|
|
45
|
+
): () => void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function isMultiproviderService(value: unknown): value is MultiproviderService {
|
|
49
|
+
if (typeof value !== "object" || value === null) return false;
|
|
50
|
+
const candidate = value as Partial<MultiproviderService>;
|
|
51
|
+
return (
|
|
52
|
+
typeof candidate.getActiveAccount === "function" &&
|
|
53
|
+
typeof candidate.resolveActiveAccountAuth === "function" &&
|
|
54
|
+
typeof candidate.onActiveAccountChanged === "function"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let activeService: MultiproviderService | undefined;
|
|
59
|
+
|
|
60
|
+
export function setActiveMultiproviderService(service: MultiproviderService | undefined): void {
|
|
61
|
+
activeService = service;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getActiveMultiproviderService(): MultiproviderService | undefined {
|
|
65
|
+
return activeService;
|
|
66
|
+
}
|