@victor-software-house/pi-multicodex 2.0.7 → 2.0.9
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 +1 -0
- package/abort-utils.ts +9 -24
- package/account-manager.ts +96 -17
- package/auth.ts +2 -3
- package/commands.ts +4 -8
- package/package.json +8 -4
- package/provider.ts +17 -31
- package/status.ts +8 -19
- package/storage.ts +2 -7
- package/stream-wrapper.ts +10 -51
package/README.md
CHANGED
|
@@ -78,6 +78,7 @@ You can customize which fields appear and their ordering with `/multicodex foote
|
|
|
78
78
|
- **Token refresh.** OAuth tokens are refreshed before expiry so requests do not fail due to stale credentials.
|
|
79
79
|
- **Usage tracking.** Usage data is fetched from the Codex API and cached for 5 minutes per account. The footer renders cached data immediately and refreshes in the background.
|
|
80
80
|
- **Quota cooldown.** When an account is exhausted, it stays on cooldown until its next known reset time (or 1 hour if the reset time is unknown).
|
|
81
|
+
- **Shared utility seams.** Provider mirroring, stream primitives, and `~/.pi/agent/*` path helpers are shared with `pi-credential-vault` through `@victor-software-house/pi-provider-utils`. MultiCodex still owns account storage, token policy, footer behavior, and command UX.
|
|
81
82
|
|
|
82
83
|
## Local development
|
|
83
84
|
|
package/abort-utils.ts
CHANGED
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
signal?.addEventListener("abort", () => controller.abort(), { once: true });
|
|
11
|
-
return controller;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function createTimeoutController(
|
|
15
|
-
signal: AbortSignal | undefined,
|
|
16
|
-
timeoutMs: number,
|
|
17
|
-
): { controller: AbortController; clear: () => void } {
|
|
18
|
-
const controller = createLinkedAbortController(signal);
|
|
19
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
20
|
-
return {
|
|
21
|
-
controller,
|
|
22
|
-
clear: () => clearTimeout(timeout),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Re-export abort controller helpers from the shared package.
|
|
3
|
+
*
|
|
4
|
+
* Existing imports within this package continue to work unchanged.
|
|
5
|
+
*/
|
|
6
|
+
export {
|
|
7
|
+
createLinkedAbortController,
|
|
8
|
+
createTimeoutController,
|
|
9
|
+
} from "pi-provider-utils/streams";
|
package/account-manager.ts
CHANGED
|
@@ -2,6 +2,8 @@ import {
|
|
|
2
2
|
type OAuthCredentials,
|
|
3
3
|
refreshOpenAICodexToken,
|
|
4
4
|
} from "@mariozechner/pi-ai/oauth";
|
|
5
|
+
import { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { normalizeUnknownError } from "pi-provider-utils/streams";
|
|
5
7
|
import { loadImportedOpenAICodexAuth } from "./auth";
|
|
6
8
|
import { isAccountAvailable, pickBestAccount } from "./selection";
|
|
7
9
|
import {
|
|
@@ -20,14 +22,10 @@ const QUOTA_COOLDOWN_MS = 60 * 60 * 1000;
|
|
|
20
22
|
type WarningHandler = (message: string) => void;
|
|
21
23
|
type StateChangeHandler = () => void;
|
|
22
24
|
|
|
23
|
-
function getErrorMessage(error: unknown): string {
|
|
24
|
-
if (error instanceof Error) return error.message;
|
|
25
|
-
return typeof error === "string" ? error : JSON.stringify(error);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
25
|
export class AccountManager {
|
|
29
26
|
private data: StorageData;
|
|
30
27
|
private usageCache = new Map<string, CodexUsageSnapshot>();
|
|
28
|
+
private refreshPromises = new Map<string, Promise<string>>();
|
|
31
29
|
private warningHandler?: WarningHandler;
|
|
32
30
|
private manualEmail?: string;
|
|
33
31
|
private stateChangeHandlers = new Set<StateChangeHandler>();
|
|
@@ -258,7 +256,7 @@ export class AccountManager {
|
|
|
258
256
|
return usage;
|
|
259
257
|
} catch (error) {
|
|
260
258
|
this.warningHandler?.(
|
|
261
|
-
`Multicodex: failed to fetch usage for ${account.email}: ${
|
|
259
|
+
`Multicodex: failed to fetch usage for ${account.email}: ${normalizeUnknownError(
|
|
262
260
|
error,
|
|
263
261
|
)}`,
|
|
264
262
|
);
|
|
@@ -346,17 +344,98 @@ export class AccountManager {
|
|
|
346
344
|
return account.accessToken;
|
|
347
345
|
}
|
|
348
346
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
account.
|
|
352
|
-
|
|
353
|
-
const accountId =
|
|
354
|
-
typeof result.accountId === "string" ? result.accountId : undefined;
|
|
355
|
-
if (accountId) {
|
|
356
|
-
account.accountId = accountId;
|
|
347
|
+
// For the imported pi account, delegate to AuthStorage so we share pi's
|
|
348
|
+
// file lock and never race with pi's own refresh path.
|
|
349
|
+
if (account.importSource === "pi-openai-codex") {
|
|
350
|
+
return this.ensureValidTokenForImportedAccount(account);
|
|
357
351
|
}
|
|
358
|
-
|
|
359
|
-
this.
|
|
360
|
-
|
|
352
|
+
|
|
353
|
+
const inflight = this.refreshPromises.get(account.email);
|
|
354
|
+
if (inflight) {
|
|
355
|
+
return inflight;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const promise = (async () => {
|
|
359
|
+
try {
|
|
360
|
+
const result = await refreshOpenAICodexToken(account.refreshToken);
|
|
361
|
+
account.accessToken = result.access;
|
|
362
|
+
account.refreshToken = result.refresh;
|
|
363
|
+
account.expiresAt = result.expires;
|
|
364
|
+
const accountId =
|
|
365
|
+
typeof result.accountId === "string" ? result.accountId : undefined;
|
|
366
|
+
if (accountId) {
|
|
367
|
+
account.accountId = accountId;
|
|
368
|
+
}
|
|
369
|
+
this.save();
|
|
370
|
+
this.notifyStateChanged();
|
|
371
|
+
return account.accessToken;
|
|
372
|
+
} finally {
|
|
373
|
+
this.refreshPromises.delete(account.email);
|
|
374
|
+
}
|
|
375
|
+
})();
|
|
376
|
+
|
|
377
|
+
this.refreshPromises.set(account.email, promise);
|
|
378
|
+
return promise;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Refresh path for the imported pi account.
|
|
383
|
+
*
|
|
384
|
+
* Uses AuthStorage so our refresh is serialised by the same file lock that
|
|
385
|
+
* pi's own credential refresh uses. This prevents "refresh_token_reused"
|
|
386
|
+
* errors caused by pi and multicodex both refreshing the same token
|
|
387
|
+
* simultaneously.
|
|
388
|
+
*/
|
|
389
|
+
private async ensureValidTokenForImportedAccount(
|
|
390
|
+
account: Account,
|
|
391
|
+
): Promise<string> {
|
|
392
|
+
// Check if pi already refreshed since our last sync.
|
|
393
|
+
const latest = await loadImportedOpenAICodexAuth();
|
|
394
|
+
if (latest && Date.now() < latest.credentials.expires - 5 * 60 * 1000) {
|
|
395
|
+
account.accessToken = latest.credentials.access;
|
|
396
|
+
account.refreshToken = latest.credentials.refresh;
|
|
397
|
+
account.expiresAt = latest.credentials.expires;
|
|
398
|
+
account.importFingerprint = latest.fingerprint;
|
|
399
|
+
const accountId =
|
|
400
|
+
typeof latest.credentials.accountId === "string"
|
|
401
|
+
? latest.credentials.accountId
|
|
402
|
+
: undefined;
|
|
403
|
+
if (accountId) {
|
|
404
|
+
account.accountId = accountId;
|
|
405
|
+
}
|
|
406
|
+
this.save();
|
|
407
|
+
this.notifyStateChanged();
|
|
408
|
+
return account.accessToken;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Both our copy and auth.json are expired — let AuthStorage refresh with
|
|
412
|
+
// its file lock so only one caller (us or pi) fires the API call.
|
|
413
|
+
const authStorage = AuthStorage.create();
|
|
414
|
+
const apiKey = await authStorage.getApiKey("openai-codex");
|
|
415
|
+
if (!apiKey) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
"OpenAI Codex: token refresh failed — please re-authenticate with /login",
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Read the refreshed tokens back from auth.json.
|
|
422
|
+
const refreshed = await loadImportedOpenAICodexAuth();
|
|
423
|
+
if (refreshed) {
|
|
424
|
+
account.accessToken = refreshed.credentials.access;
|
|
425
|
+
account.refreshToken = refreshed.credentials.refresh;
|
|
426
|
+
account.expiresAt = refreshed.credentials.expires;
|
|
427
|
+
account.importFingerprint = refreshed.fingerprint;
|
|
428
|
+
const accountId =
|
|
429
|
+
typeof refreshed.credentials.accountId === "string"
|
|
430
|
+
? refreshed.credentials.accountId
|
|
431
|
+
: undefined;
|
|
432
|
+
if (accountId) {
|
|
433
|
+
account.accountId = accountId;
|
|
434
|
+
}
|
|
435
|
+
this.save();
|
|
436
|
+
this.notifyStateChanged();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return apiKey;
|
|
361
440
|
}
|
|
362
441
|
}
|
package/auth.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
2
|
import type { OAuthCredentials } from "@mariozechner/pi-ai/oauth";
|
|
3
|
+
import { getAgentAuthPath } from "pi-provider-utils/agent-paths";
|
|
5
4
|
|
|
6
|
-
const AUTH_FILE =
|
|
5
|
+
const AUTH_FILE = getAgentAuthPath();
|
|
7
6
|
const IMPORTED_ACCOUNT_PREFIX = "OpenAI Codex";
|
|
8
7
|
|
|
9
8
|
interface AuthEntry {
|
package/commands.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { promises as fs, constants as fsConstants } from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { loginOpenAICodex } from "@mariozechner/pi-ai/oauth";
|
|
5
4
|
import type {
|
|
@@ -15,13 +14,15 @@ import {
|
|
|
15
14
|
SelectList,
|
|
16
15
|
Text,
|
|
17
16
|
} from "@mariozechner/pi-tui";
|
|
17
|
+
import { getAgentSettingsPath } from "pi-provider-utils/agent-paths";
|
|
18
|
+
import { normalizeUnknownError } from "pi-provider-utils/streams";
|
|
18
19
|
import type { AccountManager } from "./account-manager";
|
|
19
20
|
import { openLoginInBrowser } from "./browser";
|
|
20
21
|
import type { createUsageStatusController } from "./status";
|
|
21
22
|
import { STORAGE_FILE } from "./storage";
|
|
22
23
|
import { formatResetAt, isUsageUntouched } from "./usage";
|
|
23
24
|
|
|
24
|
-
const SETTINGS_FILE =
|
|
25
|
+
const SETTINGS_FILE = getAgentSettingsPath();
|
|
25
26
|
const NO_ACCOUNTS_MESSAGE =
|
|
26
27
|
"No managed accounts found. Use /multicodex use <identifier> first.";
|
|
27
28
|
const HELP_TEXT =
|
|
@@ -46,11 +47,6 @@ type AccountPanelResult =
|
|
|
46
47
|
| { action: "remove"; email: string }
|
|
47
48
|
| undefined;
|
|
48
49
|
|
|
49
|
-
function getErrorMessage(error: unknown): string {
|
|
50
|
-
if (error instanceof Error) return error.message;
|
|
51
|
-
return typeof error === "string" ? error : JSON.stringify(error);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
50
|
function toAutocompleteItems(values: readonly string[]): AutocompleteItem[] {
|
|
55
51
|
return values.map((value) => ({ value, label: value }));
|
|
56
52
|
}
|
|
@@ -202,7 +198,7 @@ async function loginAndActivateAccount(
|
|
|
202
198
|
ctx.ui.notify(`Now using ${identifier}`, "info");
|
|
203
199
|
return true;
|
|
204
200
|
} catch (error) {
|
|
205
|
-
ctx.ui.notify(`Login failed: ${
|
|
201
|
+
ctx.ui.notify(`Login failed: ${normalizeUnknownError(error)}`, "error");
|
|
206
202
|
return false;
|
|
207
203
|
}
|
|
208
204
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@victor-software-house/pi-multicodex",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
4
4
|
"description": "Codex account rotation extension for pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -78,9 +78,9 @@
|
|
|
78
78
|
"@biomejs/biome": "^2.4.7",
|
|
79
79
|
"@commitlint/cli": "^20.4.4",
|
|
80
80
|
"@commitlint/config-conventional": "^20.4.4",
|
|
81
|
-
"@mariozechner/pi-ai": "^0.
|
|
82
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
83
|
-
"@mariozechner/pi-tui": "^0.
|
|
81
|
+
"@mariozechner/pi-ai": "^0.63.1",
|
|
82
|
+
"@mariozechner/pi-coding-agent": "^0.63.1",
|
|
83
|
+
"@mariozechner/pi-tui": "^0.63.1",
|
|
84
84
|
"@semantic-release/changelog": "^6.0.3",
|
|
85
85
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
86
86
|
"@semantic-release/git": "^10.0.1",
|
|
@@ -89,11 +89,15 @@
|
|
|
89
89
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
90
90
|
"@types/node": "^25.5.0",
|
|
91
91
|
"@typescript/native-preview": "7.0.0-dev.20260314.1",
|
|
92
|
+
"conventional-changelog-conventionalcommits": "^9.3.0",
|
|
92
93
|
"semantic-release": "^25.0.3",
|
|
93
94
|
"typescript": "^5.9.3",
|
|
94
95
|
"vitest": "^4.1.0"
|
|
95
96
|
},
|
|
96
97
|
"engines": {
|
|
97
98
|
"node": "24.14.0"
|
|
99
|
+
},
|
|
100
|
+
"dependencies": {
|
|
101
|
+
"pi-provider-utils": "^0.0.0"
|
|
98
102
|
}
|
|
99
103
|
}
|
package/provider.ts
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type AssistantMessageEventStream,
|
|
4
|
-
type Context,
|
|
5
|
-
getApiProvider,
|
|
6
|
-
getModels,
|
|
7
|
-
type Model,
|
|
8
|
-
type SimpleStreamOptions,
|
|
9
|
-
} from "@mariozechner/pi-ai";
|
|
1
|
+
import { getApiProvider } from "@mariozechner/pi-ai";
|
|
2
|
+
import { mirrorProvider } from "pi-provider-utils/providers";
|
|
10
3
|
import type { AccountManager } from "./account-manager";
|
|
11
4
|
import { createStreamWrapper } from "./stream-wrapper";
|
|
12
5
|
|
|
@@ -31,32 +24,25 @@ export function getOpenAICodexMirror(): {
|
|
|
31
24
|
baseUrl: string;
|
|
32
25
|
models: ProviderModelDef[];
|
|
33
26
|
} {
|
|
34
|
-
const
|
|
27
|
+
const mirror = mirrorProvider("openai-codex");
|
|
28
|
+
if (!mirror) {
|
|
29
|
+
return { baseUrl: "https://chatgpt.com/backend-api", models: [] };
|
|
30
|
+
}
|
|
35
31
|
return {
|
|
36
|
-
baseUrl:
|
|
37
|
-
models:
|
|
38
|
-
id:
|
|
39
|
-
name:
|
|
40
|
-
reasoning:
|
|
41
|
-
input:
|
|
42
|
-
cost:
|
|
43
|
-
contextWindow:
|
|
44
|
-
maxTokens:
|
|
32
|
+
baseUrl: mirror.baseUrl,
|
|
33
|
+
models: mirror.models.map((m) => ({
|
|
34
|
+
id: m.id,
|
|
35
|
+
name: m.name,
|
|
36
|
+
reasoning: m.reasoning,
|
|
37
|
+
input: [...m.input],
|
|
38
|
+
cost: { ...m.cost },
|
|
39
|
+
contextWindow: m.contextWindow,
|
|
40
|
+
maxTokens: m.maxTokens,
|
|
45
41
|
})),
|
|
46
42
|
};
|
|
47
43
|
}
|
|
48
44
|
|
|
49
|
-
export function buildMulticodexProviderConfig(accountManager: AccountManager)
|
|
50
|
-
baseUrl: string;
|
|
51
|
-
apiKey: string;
|
|
52
|
-
api: "openai-codex-responses";
|
|
53
|
-
streamSimple: (
|
|
54
|
-
model: Model<Api>,
|
|
55
|
-
context: Context,
|
|
56
|
-
options?: SimpleStreamOptions,
|
|
57
|
-
) => AssistantMessageEventStream;
|
|
58
|
-
models: ProviderModelDef[];
|
|
59
|
-
} {
|
|
45
|
+
export function buildMulticodexProviderConfig(accountManager: AccountManager) {
|
|
60
46
|
const mirror = getOpenAICodexMirror();
|
|
61
47
|
const baseProvider = getApiProvider("openai-codex-responses");
|
|
62
48
|
if (!baseProvider) {
|
|
@@ -68,7 +54,7 @@ export function buildMulticodexProviderConfig(accountManager: AccountManager): {
|
|
|
68
54
|
return {
|
|
69
55
|
baseUrl: mirror.baseUrl,
|
|
70
56
|
apiKey: "managed-by-extension",
|
|
71
|
-
api: "openai-codex-responses",
|
|
57
|
+
api: "openai-codex-responses" as const,
|
|
72
58
|
streamSimple: createStreamWrapper(accountManager, baseProvider),
|
|
73
59
|
models: mirror.models,
|
|
74
60
|
};
|
package/status.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
1
|
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
5
2
|
import type {
|
|
6
3
|
ExtensionCommandContext,
|
|
@@ -13,13 +10,18 @@ import {
|
|
|
13
10
|
SettingsList,
|
|
14
11
|
Text,
|
|
15
12
|
} from "@mariozechner/pi-tui";
|
|
13
|
+
import {
|
|
14
|
+
getAgentSettingsPath,
|
|
15
|
+
readJsonObjectFileAsync,
|
|
16
|
+
writeJsonObjectFileAsync,
|
|
17
|
+
} from "pi-provider-utils/agent-paths";
|
|
16
18
|
import type { AccountManager } from "./account-manager";
|
|
17
19
|
import { PROVIDER_ID } from "./provider";
|
|
18
20
|
import type { CodexUsageSnapshot } from "./usage";
|
|
19
21
|
|
|
20
22
|
const STATUS_KEY = "multicodex-usage";
|
|
21
23
|
const SETTINGS_KEY = "pi-multicodex";
|
|
22
|
-
const SETTINGS_FILE =
|
|
24
|
+
const SETTINGS_FILE = getAgentSettingsPath();
|
|
23
25
|
const REFRESH_INTERVAL_MS = 60_000;
|
|
24
26
|
const MODEL_SELECT_REFRESH_DEBOUNCE_MS = 250;
|
|
25
27
|
const UNKNOWN_PERCENT = "--";
|
|
@@ -90,26 +92,13 @@ function normalizePreferences(value: unknown): FooterPreferences {
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
async function readSettingsFile(): Promise<Record<string, unknown>> {
|
|
93
|
-
|
|
94
|
-
const raw = await fs.readFile(SETTINGS_FILE, "utf8");
|
|
95
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
96
|
-
return asObject(parsed) ?? {};
|
|
97
|
-
} catch (error) {
|
|
98
|
-
const withCode = error as Error & { code?: string };
|
|
99
|
-
if (withCode.code === "ENOENT") return {};
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
95
|
+
return readJsonObjectFileAsync(SETTINGS_FILE);
|
|
102
96
|
}
|
|
103
97
|
|
|
104
98
|
async function writeSettingsFile(
|
|
105
99
|
settings: Record<string, unknown>,
|
|
106
100
|
): Promise<void> {
|
|
107
|
-
await
|
|
108
|
-
await fs.writeFile(
|
|
109
|
-
SETTINGS_FILE,
|
|
110
|
-
`${JSON.stringify(settings, null, 2)}\n`,
|
|
111
|
-
"utf8",
|
|
112
|
-
);
|
|
101
|
+
await writeJsonObjectFileAsync(SETTINGS_FILE, settings);
|
|
113
102
|
}
|
|
114
103
|
|
|
115
104
|
export async function loadFooterPreferences(): Promise<FooterPreferences> {
|
package/storage.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
2
|
import * as path from "node:path";
|
|
3
|
+
import { getAgentPath } from "pi-provider-utils/agent-paths";
|
|
4
4
|
|
|
5
5
|
export interface Account {
|
|
6
6
|
email: string;
|
|
@@ -19,12 +19,7 @@ export interface StorageData {
|
|
|
19
19
|
activeEmail?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export const STORAGE_FILE =
|
|
23
|
-
os.homedir(),
|
|
24
|
-
".pi",
|
|
25
|
-
"agent",
|
|
26
|
-
"codex-accounts.json",
|
|
27
|
-
);
|
|
22
|
+
export const STORAGE_FILE = getAgentPath("codex-accounts.json");
|
|
28
23
|
|
|
29
24
|
export function loadStorage(): StorageData {
|
|
30
25
|
try {
|
package/stream-wrapper.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Api,
|
|
3
|
-
type AssistantMessage,
|
|
4
3
|
type AssistantMessageEvent,
|
|
5
4
|
type AssistantMessageEventStream,
|
|
6
5
|
type Context,
|
|
@@ -8,7 +7,12 @@ import {
|
|
|
8
7
|
type Model,
|
|
9
8
|
type SimpleStreamOptions,
|
|
10
9
|
} from "@mariozechner/pi-ai";
|
|
11
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
createErrorAssistantMessage,
|
|
12
|
+
createLinkedAbortController,
|
|
13
|
+
normalizeUnknownError,
|
|
14
|
+
rewriteProviderOnEvent,
|
|
15
|
+
} from "pi-provider-utils/streams";
|
|
12
16
|
import type { AccountManager } from "./account-manager";
|
|
13
17
|
import { isQuotaErrorMessage } from "./quota";
|
|
14
18
|
|
|
@@ -22,51 +26,6 @@ type ApiProviderRef = {
|
|
|
22
26
|
) => AssistantMessageEventStream;
|
|
23
27
|
};
|
|
24
28
|
|
|
25
|
-
function withProvider(
|
|
26
|
-
event: AssistantMessageEvent,
|
|
27
|
-
provider: string,
|
|
28
|
-
): AssistantMessageEvent {
|
|
29
|
-
if ("partial" in event) {
|
|
30
|
-
return { ...event, partial: { ...event.partial, provider } };
|
|
31
|
-
}
|
|
32
|
-
if (event.type === "done") {
|
|
33
|
-
return { ...event, message: { ...event.message, provider } };
|
|
34
|
-
}
|
|
35
|
-
if (event.type === "error") {
|
|
36
|
-
return { ...event, error: { ...event.error, provider } };
|
|
37
|
-
}
|
|
38
|
-
return event;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function createErrorAssistantMessage(
|
|
42
|
-
model: Model<Api>,
|
|
43
|
-
message: string,
|
|
44
|
-
): AssistantMessage {
|
|
45
|
-
return {
|
|
46
|
-
role: "assistant",
|
|
47
|
-
content: [],
|
|
48
|
-
api: model.api,
|
|
49
|
-
provider: model.provider,
|
|
50
|
-
model: model.id,
|
|
51
|
-
usage: {
|
|
52
|
-
input: 0,
|
|
53
|
-
output: 0,
|
|
54
|
-
cacheRead: 0,
|
|
55
|
-
cacheWrite: 0,
|
|
56
|
-
totalTokens: 0,
|
|
57
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
58
|
-
},
|
|
59
|
-
stopReason: "error",
|
|
60
|
-
errorMessage: message,
|
|
61
|
-
timestamp: Date.now(),
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function getErrorMessage(error: unknown): string {
|
|
66
|
-
if (error instanceof Error) return error.message;
|
|
67
|
-
return typeof error === "string" ? error : JSON.stringify(error);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
29
|
export function createStreamWrapper(
|
|
71
30
|
accountManager: AccountManager,
|
|
72
31
|
baseProvider: ApiProviderRef,
|
|
@@ -151,13 +110,13 @@ export function createStreamWrapper(
|
|
|
151
110
|
break;
|
|
152
111
|
}
|
|
153
112
|
|
|
154
|
-
stream.push(
|
|
113
|
+
stream.push(rewriteProviderOnEvent(event, model.provider));
|
|
155
114
|
stream.end();
|
|
156
115
|
return;
|
|
157
116
|
}
|
|
158
117
|
|
|
159
118
|
forwardedAny = true;
|
|
160
|
-
stream.push(
|
|
119
|
+
stream.push(rewriteProviderOnEvent(event, model.provider));
|
|
161
120
|
|
|
162
121
|
if (event.type === "done") {
|
|
163
122
|
stream.end();
|
|
@@ -173,7 +132,7 @@ export function createStreamWrapper(
|
|
|
173
132
|
return;
|
|
174
133
|
}
|
|
175
134
|
} catch (error) {
|
|
176
|
-
const message =
|
|
135
|
+
const message = normalizeUnknownError(error);
|
|
177
136
|
const errorEvent: AssistantMessageEvent = {
|
|
178
137
|
type: "error",
|
|
179
138
|
reason: "error",
|
|
@@ -182,7 +141,7 @@ export function createStreamWrapper(
|
|
|
182
141
|
`Multicodex failed: ${message}`,
|
|
183
142
|
),
|
|
184
143
|
};
|
|
185
|
-
stream.push(
|
|
144
|
+
stream.push(rewriteProviderOnEvent(errorEvent, model.provider));
|
|
186
145
|
stream.end();
|
|
187
146
|
}
|
|
188
147
|
})();
|