@victor-software-house/pi-multicodex 1.0.3 → 1.0.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.
- package/LICENSE +1 -1
- package/README.md +42 -20
- package/abort-utils.ts +24 -0
- package/account-manager.ts +262 -0
- package/browser.ts +34 -0
- package/commands.ts +150 -0
- package/extension.ts +63 -0
- package/hooks.ts +22 -0
- package/index.ts +28 -985
- package/package.json +20 -3
- package/provider.ts +75 -0
- package/quota.ts +5 -0
- package/selection.ts +69 -0
- package/status.ts +462 -0
- package/storage.ts +49 -0
- package/stream-wrapper.ts +191 -0
- package/usage-client.ts +50 -0
- package/usage.ts +86 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Api,
|
|
3
|
+
type AssistantMessage,
|
|
4
|
+
type AssistantMessageEvent,
|
|
5
|
+
type AssistantMessageEventStream,
|
|
6
|
+
type Context,
|
|
7
|
+
createAssistantMessageEventStream,
|
|
8
|
+
type Model,
|
|
9
|
+
type SimpleStreamOptions,
|
|
10
|
+
} from "@mariozechner/pi-ai";
|
|
11
|
+
import { createLinkedAbortController } from "./abort-utils";
|
|
12
|
+
import type { AccountManager } from "./account-manager";
|
|
13
|
+
import { isQuotaErrorMessage } from "./quota";
|
|
14
|
+
|
|
15
|
+
const MAX_ROTATION_RETRIES = 5;
|
|
16
|
+
|
|
17
|
+
type ApiProviderRef = {
|
|
18
|
+
streamSimple: (
|
|
19
|
+
model: Model<Api>,
|
|
20
|
+
context: Context,
|
|
21
|
+
options?: SimpleStreamOptions,
|
|
22
|
+
) => AssistantMessageEventStream;
|
|
23
|
+
};
|
|
24
|
+
|
|
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
|
+
export function createStreamWrapper(
|
|
71
|
+
accountManager: AccountManager,
|
|
72
|
+
baseProvider: ApiProviderRef,
|
|
73
|
+
) {
|
|
74
|
+
return (
|
|
75
|
+
model: Model<Api>,
|
|
76
|
+
context: Context,
|
|
77
|
+
options?: SimpleStreamOptions,
|
|
78
|
+
): AssistantMessageEventStream => {
|
|
79
|
+
const stream = createAssistantMessageEventStream();
|
|
80
|
+
|
|
81
|
+
(async () => {
|
|
82
|
+
try {
|
|
83
|
+
const excludedEmails = new Set<string>();
|
|
84
|
+
for (let attempt = 0; attempt <= MAX_ROTATION_RETRIES; attempt++) {
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const manual = accountManager.getAvailableManualAccount({
|
|
87
|
+
excludeEmails: excludedEmails,
|
|
88
|
+
now,
|
|
89
|
+
});
|
|
90
|
+
const usingManual = Boolean(manual);
|
|
91
|
+
let account = manual;
|
|
92
|
+
if (!account) {
|
|
93
|
+
if (accountManager.hasManualAccount()) {
|
|
94
|
+
accountManager.clearManualAccount();
|
|
95
|
+
}
|
|
96
|
+
account = await accountManager.activateBestAccount({
|
|
97
|
+
excludeEmails: excludedEmails,
|
|
98
|
+
signal: options?.signal,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (!account) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"No available Multicodex accounts. Please use /multicodex-login.",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const token = await accountManager.ensureValidToken(account);
|
|
108
|
+
const abortController = createLinkedAbortController(options?.signal);
|
|
109
|
+
|
|
110
|
+
const internalModel: Model<"openai-codex-responses"> = {
|
|
111
|
+
...(model as Model<"openai-codex-responses">),
|
|
112
|
+
provider: "openai-codex",
|
|
113
|
+
api: "openai-codex-responses",
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const inner = baseProvider.streamSimple(
|
|
117
|
+
{
|
|
118
|
+
...internalModel,
|
|
119
|
+
headers: {
|
|
120
|
+
...(internalModel.headers || {}),
|
|
121
|
+
"X-Multicodex-Account": account.email,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
context,
|
|
125
|
+
{
|
|
126
|
+
...options,
|
|
127
|
+
apiKey: token,
|
|
128
|
+
signal: abortController.signal,
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
let forwardedAny = false;
|
|
133
|
+
let retry = false;
|
|
134
|
+
|
|
135
|
+
for await (const event of inner) {
|
|
136
|
+
if (event.type === "error") {
|
|
137
|
+
const msg = event.error.errorMessage || "";
|
|
138
|
+
const isQuota = isQuotaErrorMessage(msg);
|
|
139
|
+
|
|
140
|
+
if (isQuota && !forwardedAny && attempt < MAX_ROTATION_RETRIES) {
|
|
141
|
+
await accountManager.handleQuotaExceeded(account, {
|
|
142
|
+
signal: options?.signal,
|
|
143
|
+
});
|
|
144
|
+
if (usingManual) {
|
|
145
|
+
accountManager.clearManualAccount();
|
|
146
|
+
}
|
|
147
|
+
excludedEmails.add(account.email);
|
|
148
|
+
abortController.abort();
|
|
149
|
+
retry = true;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
stream.push(withProvider(event, model.provider));
|
|
154
|
+
stream.end();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
forwardedAny = true;
|
|
159
|
+
stream.push(withProvider(event, model.provider));
|
|
160
|
+
|
|
161
|
+
if (event.type === "done") {
|
|
162
|
+
stream.end();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (retry) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
stream.end();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const message = getErrorMessage(error);
|
|
176
|
+
const errorEvent: AssistantMessageEvent = {
|
|
177
|
+
type: "error",
|
|
178
|
+
reason: "error",
|
|
179
|
+
error: createErrorAssistantMessage(
|
|
180
|
+
model,
|
|
181
|
+
`Multicodex failed: ${message}`,
|
|
182
|
+
),
|
|
183
|
+
};
|
|
184
|
+
stream.push(withProvider(errorEvent, model.provider));
|
|
185
|
+
stream.end();
|
|
186
|
+
}
|
|
187
|
+
})();
|
|
188
|
+
|
|
189
|
+
return stream;
|
|
190
|
+
};
|
|
191
|
+
}
|
package/usage-client.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createTimeoutController } from "./abort-utils";
|
|
2
|
+
import { type CodexUsageSnapshot, parseCodexUsageResponse } from "./usage";
|
|
3
|
+
|
|
4
|
+
interface WhamUsageResponse {
|
|
5
|
+
rate_limit?: {
|
|
6
|
+
primary_window?: {
|
|
7
|
+
reset_at?: number;
|
|
8
|
+
used_percent?: number;
|
|
9
|
+
};
|
|
10
|
+
secondary_window?: {
|
|
11
|
+
reset_at?: number;
|
|
12
|
+
used_percent?: number;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function fetchCodexUsage(
|
|
18
|
+
accessToken: string,
|
|
19
|
+
accountId: string | undefined,
|
|
20
|
+
options?: { signal?: AbortSignal; timeoutMs?: number },
|
|
21
|
+
): Promise<CodexUsageSnapshot> {
|
|
22
|
+
const { controller, clear } = createTimeoutController(
|
|
23
|
+
options?.signal,
|
|
24
|
+
options?.timeoutMs ?? 10_000,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const headers: Record<string, string> = {
|
|
29
|
+
Authorization: `Bearer ${accessToken}`,
|
|
30
|
+
Accept: "application/json",
|
|
31
|
+
};
|
|
32
|
+
if (accountId) {
|
|
33
|
+
headers["ChatGPT-Account-Id"] = accountId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const response = await fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
37
|
+
headers,
|
|
38
|
+
signal: controller.signal,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error(`Usage request failed: ${response.status}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const data = (await response.json()) as WhamUsageResponse;
|
|
46
|
+
return { ...parseCodexUsageResponse(data), fetchedAt: Date.now() };
|
|
47
|
+
} finally {
|
|
48
|
+
clear();
|
|
49
|
+
}
|
|
50
|
+
}
|
package/usage.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
interface CodexUsageWindow {
|
|
2
|
+
usedPercent?: number;
|
|
3
|
+
resetAt?: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface CodexUsageSnapshot {
|
|
7
|
+
primary?: CodexUsageWindow;
|
|
8
|
+
secondary?: CodexUsageWindow;
|
|
9
|
+
fetchedAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface WhamUsageResponse {
|
|
13
|
+
rate_limit?: {
|
|
14
|
+
primary_window?: WhamUsageWindow;
|
|
15
|
+
secondary_window?: WhamUsageWindow;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type WhamUsageWindow = {
|
|
20
|
+
reset_at?: number;
|
|
21
|
+
used_percent?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function normalizeUsedPercent(value?: number): number | undefined {
|
|
25
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
26
|
+
return Math.min(100, Math.max(0, value));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeResetAt(value?: number): number | undefined {
|
|
30
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
31
|
+
return value * 1000;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseUsageWindow(
|
|
35
|
+
window?: WhamUsageWindow,
|
|
36
|
+
): CodexUsageWindow | undefined {
|
|
37
|
+
if (!window) return undefined;
|
|
38
|
+
const usedPercent = normalizeUsedPercent(window.used_percent);
|
|
39
|
+
const resetAt = normalizeResetAt(window.reset_at);
|
|
40
|
+
if (usedPercent === undefined && resetAt === undefined) return undefined;
|
|
41
|
+
return { usedPercent, resetAt };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseCodexUsageResponse(
|
|
45
|
+
data: WhamUsageResponse,
|
|
46
|
+
): Omit<CodexUsageSnapshot, "fetchedAt"> {
|
|
47
|
+
return {
|
|
48
|
+
primary: parseUsageWindow(data.rate_limit?.primary_window),
|
|
49
|
+
secondary: parseUsageWindow(data.rate_limit?.secondary_window),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isUsageUntouched(usage?: CodexUsageSnapshot): boolean {
|
|
54
|
+
const primary = usage?.primary?.usedPercent;
|
|
55
|
+
const secondary = usage?.secondary?.usedPercent;
|
|
56
|
+
if (primary === undefined || secondary === undefined) return false;
|
|
57
|
+
return primary === 0 && secondary === 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getNextResetAt(usage?: CodexUsageSnapshot): number | undefined {
|
|
61
|
+
const candidates = [
|
|
62
|
+
usage?.primary?.resetAt,
|
|
63
|
+
usage?.secondary?.resetAt,
|
|
64
|
+
].filter((value): value is number => typeof value === "number");
|
|
65
|
+
if (candidates.length === 0) return undefined;
|
|
66
|
+
return Math.min(...candidates);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getWeeklyResetAt(
|
|
70
|
+
usage?: CodexUsageSnapshot,
|
|
71
|
+
): number | undefined {
|
|
72
|
+
const resetAt = usage?.secondary?.resetAt;
|
|
73
|
+
return typeof resetAt === "number" ? resetAt : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function formatResetAt(resetAt?: number): string {
|
|
77
|
+
if (!resetAt) return "unknown";
|
|
78
|
+
const diffMs = resetAt - Date.now();
|
|
79
|
+
if (diffMs <= 0) return "now";
|
|
80
|
+
const diffMinutes = Math.max(1, Math.round(diffMs / 60000));
|
|
81
|
+
if (diffMinutes < 60) return `in ${diffMinutes}m`;
|
|
82
|
+
const diffHours = Math.round(diffMinutes / 60);
|
|
83
|
+
if (diffHours < 48) return `in ${diffHours}h`;
|
|
84
|
+
const diffDays = Math.round(diffHours / 24);
|
|
85
|
+
return `in ${diffDays}d`;
|
|
86
|
+
}
|