@jameslovespancakes/pi-plus 1.0.11 → 1.0.13
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 +67 -22
- package/config/skills/workflow-code-review-actions/SKILL.md +44 -0
- package/package.json +9 -4
- package/src/core/accounts/oauth-pool.ts +137 -0
- package/src/core/accounts/registry.ts +12 -25
- package/src/core/accounts/routing.ts +144 -0
- package/src/core/anthropic/client-identity.ts +9 -64
- package/src/core/anthropic/identity.ts +54 -0
- package/src/core/anthropic/oauth.ts +3 -0
- package/src/core/anthropic/quota.ts +232 -78
- package/src/core/anthropic/routing.ts +40 -115
- package/src/core/anthropic/store.ts +11 -10
- package/src/core/catalog/quality.ts +33 -15
- package/src/core/codex/quota.ts +46 -8
- package/src/core/codex/store.ts +29 -16
- package/src/core/config.ts +16 -17
- package/src/core/store.ts +13 -12
- package/src/domains/agents/format.ts +97 -0
- package/src/domains/agents/index.ts +58 -47
- package/src/domains/compact/archive.ts +140 -0
- package/src/domains/compact/chunking.ts +166 -0
- package/src/domains/compact/index.ts +452 -0
- package/src/domains/compact/jev.ts +242 -0
- package/src/domains/compact/policy.ts +255 -0
- package/src/domains/compact/types.ts +79 -0
- package/src/domains/models/provider-picker.ts +1 -1
- package/src/domains/setup/index.ts +23 -23
- package/src/domains/subscriptions/accounts-picker.ts +61 -40
- package/src/domains/subscriptions/accounts.ts +53 -36
- package/src/domains/subscriptions/footer.ts +48 -29
- package/src/domains/subscriptions/index.ts +11 -27
- package/src/domains/subscriptions/provider.ts +79 -103
- package/src/domains/subscriptions/providers/anthropic.ts +42 -22
- package/src/domains/subscriptions/providers/codex.ts +146 -113
- package/src/domains/subscriptions/providers/hosted.ts +18 -0
- package/src/domains/subscriptions/providers/oauth-pool.ts +325 -0
- package/src/domains/subscriptions/routing.ts +19 -11
- package/src/domains/workflows/LICENSE.md +21 -0
- package/src/domains/workflows/index.ts +836 -0
- package/src/domains/workflows/runtime/advisory-challenge.ts +75 -0
- package/src/domains/workflows/runtime/advisory-evidence.ts +90 -0
- package/src/domains/workflows/runtime/advisory-schema.ts +83 -0
- package/src/domains/workflows/runtime/agent-attempt.ts +177 -0
- package/src/domains/workflows/runtime/agent-failure.ts +14 -0
- package/src/domains/workflows/runtime/agent-limits.ts +66 -0
- package/src/domains/workflows/runtime/agent-replay.ts +399 -0
- package/src/domains/workflows/runtime/agent-retry.ts +116 -0
- package/src/domains/workflows/runtime/agent-runner-types.ts +96 -0
- package/src/domains/workflows/runtime/agent-runner.ts +225 -0
- package/src/domains/workflows/runtime/agent-session-identity.ts +256 -0
- package/src/domains/workflows/runtime/agent-session-providers.ts +50 -0
- package/src/domains/workflows/runtime/agent-session.ts +382 -0
- package/src/domains/workflows/runtime/agent-skills.ts +270 -0
- package/src/domains/workflows/runtime/agent-workspace.ts +79 -0
- package/src/domains/workflows/runtime/background-workflow-tool.ts +75 -0
- package/src/domains/workflows/runtime/background-workflows.ts +492 -0
- package/src/domains/workflows/runtime/budget.ts +53 -0
- package/src/domains/workflows/runtime/cancellation.ts +87 -0
- package/src/domains/workflows/runtime/command-completions.ts +36 -0
- package/src/domains/workflows/runtime/concurrency.ts +403 -0
- package/src/domains/workflows/runtime/debug.ts +3 -0
- package/src/domains/workflows/runtime/diff-capture.ts +81 -0
- package/src/domains/workflows/runtime/discovery.ts +137 -0
- package/src/domains/workflows/runtime/dynamax-shortcuts.ts +122 -0
- package/src/domains/workflows/runtime/dynamax.ts +330 -0
- package/src/domains/workflows/runtime/engine.ts +579 -0
- package/src/domains/workflows/runtime/filesystem-error.ts +4 -0
- package/src/domains/workflows/runtime/finalizers.ts +66 -0
- package/src/domains/workflows/runtime/identity-canonicalization.ts +138 -0
- package/src/domains/workflows/runtime/identity-fingerprint.ts +15 -0
- package/src/domains/workflows/runtime/inline-workflow.ts +403 -0
- package/src/domains/workflows/runtime/journal.ts +313 -0
- package/src/domains/workflows/runtime/model-profiles.ts +310 -0
- package/src/domains/workflows/runtime/options.ts +157 -0
- package/src/domains/workflows/runtime/perf.ts +146 -0
- package/src/domains/workflows/runtime/pi-compat.ts +32 -0
- package/src/domains/workflows/runtime/process-runner.ts +260 -0
- package/src/domains/workflows/runtime/progress-types.ts +59 -0
- package/src/domains/workflows/runtime/progress.ts +400 -0
- package/src/domains/workflows/runtime/provider-usage-limit.ts +191 -0
- package/src/domains/workflows/runtime/replay-path-identity.ts +48 -0
- package/src/domains/workflows/runtime/research-contract.ts +89 -0
- package/src/domains/workflows/runtime/research-evidence.ts +289 -0
- package/src/domains/workflows/runtime/resume-context.ts +751 -0
- package/src/domains/workflows/runtime/review/code-review-orchestration.ts +24 -0
- package/src/domains/workflows/runtime/review/github-pr-comments.ts +249 -0
- package/src/domains/workflows/runtime/review/patch-validation.ts +120 -0
- package/src/domains/workflows/runtime/review/review-actions.ts +180 -0
- package/src/domains/workflows/runtime/review/review-budget.ts +45 -0
- package/src/domains/workflows/runtime/review/review-fix-workflow.ts +153 -0
- package/src/domains/workflows/runtime/review/review-format.ts +133 -0
- package/src/domains/workflows/runtime/review/review-handoff.ts +38 -0
- package/src/domains/workflows/runtime/review/review-issues.ts +87 -0
- package/src/domains/workflows/runtime/review/review-report.ts +42 -0
- package/src/domains/workflows/runtime/review/review-results-flow.ts +52 -0
- package/src/domains/workflows/runtime/review/review-results-viewer.ts +312 -0
- package/src/domains/workflows/runtime/review/review-session-coordinator.ts +153 -0
- package/src/domains/workflows/runtime/review/review-snapshot.ts +294 -0
- package/src/domains/workflows/runtime/review-diff-target.ts +130 -0
- package/src/domains/workflows/runtime/session-identity.ts +6 -0
- package/src/domains/workflows/runtime/structured-output.ts +32 -0
- package/src/domains/workflows/runtime/tool-capabilities.ts +44 -0
- package/src/domains/workflows/runtime/tool-source-identity.ts +150 -0
- package/src/domains/workflows/runtime/tree-fingerprint.ts +404 -0
- package/src/domains/workflows/runtime/types.ts +255 -0
- package/src/domains/workflows/runtime/ui/display-text.ts +28 -0
- package/src/domains/workflows/runtime/ui/dynamax-editor-decoration.ts +220 -0
- package/src/domains/workflows/runtime/ui/workflow-format.ts +165 -0
- package/src/domains/workflows/runtime/ui/workflow-inspector.ts +435 -0
- package/src/domains/workflows/runtime/ui/workflow-result-renderer.ts +171 -0
- package/src/domains/workflows/runtime/ui/workflow-viewer-layout.ts +51 -0
- package/src/domains/workflows/runtime/ui/workflow-widget.ts +78 -0
- package/src/domains/workflows/runtime/unknown-error.ts +8 -0
- package/src/domains/workflows/runtime/usage.ts +341 -0
- package/src/domains/workflows/runtime/workflow-advisory-utils.ts +245 -0
- package/src/domains/workflows/runtime/workflow-execution.ts +121 -0
- package/src/domains/workflows/runtime/workflow-module.ts +75 -0
- package/src/domains/workflows/runtime/workflow-run-background.ts +72 -0
- package/src/domains/workflows/runtime/workflow-run-controller.ts +342 -0
- package/src/domains/workflows/runtime/workflow-run-history.ts +164 -0
- package/src/domains/workflows/runtime/workflow-run-record.ts +636 -0
- package/src/domains/workflows/runtime/workflow-run-store.ts +176 -0
- package/src/domains/workflows/runtime/workflow-usage-limit-scheduler.ts +80 -0
- package/src/domains/workflows/runtime/workflows.ts +40 -0
- package/src/domains/workflows/runtime/worktree.ts +615 -0
- package/src/domains/workflows/workflows/code-review.ts +232 -0
- package/src/domains/workflows/workflows/diagnose.ts +154 -0
- package/src/domains/workflows/workflows/perf-review.ts +149 -0
- package/src/domains/workflows/workflows/refactor-scout.ts +143 -0
- package/src/domains/workflows/workflows/research.ts +169 -0
- package/src/services/usage-service.ts +6 -30
- package/src/ui/usage-bars.ts +1 -2
- package/src/core/codex/oauth.ts +0 -129
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { AccountContext, AccountProvider, ManagedAccount, RoutingMode } from "../../../core/accounts/registry.ts";
|
|
3
|
+
import { anthropicAccountIdentity } from "../../../core/anthropic/identity.ts";
|
|
3
4
|
import { authorize, exchange } from "../../../core/anthropic/oauth.ts";
|
|
5
|
+
import { ensureAccessToken } from "../../../core/anthropic/quota.ts";
|
|
4
6
|
import {
|
|
5
7
|
getRoutingMode,
|
|
6
8
|
loadAccounts,
|
|
7
9
|
saveAccount,
|
|
8
10
|
setRoutingMode,
|
|
9
|
-
type
|
|
11
|
+
type Account,
|
|
10
12
|
} from "../../../core/anthropic/store.ts";
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -22,16 +24,6 @@ import {
|
|
|
22
24
|
* the extracted store directly.
|
|
23
25
|
*/
|
|
24
26
|
|
|
25
|
-
/** The registry's vocabulary maps onto the store's routing modes. */
|
|
26
|
-
const MODE_TO_STORE: Record<RoutingMode, StoreRoutingMode> = {
|
|
27
|
-
standard: "main-first",
|
|
28
|
-
optimal: "sticky-balanced",
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
function fromStoreMode(mode: string): RoutingMode {
|
|
32
|
-
return mode === "sticky-balanced" ? "optimal" : "standard";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
27
|
async function authenticate(ctx: AccountContext): Promise<{ access: string; refresh: string; expires: number } | undefined> {
|
|
36
28
|
const auth = await authorize("max");
|
|
37
29
|
|
|
@@ -51,22 +43,45 @@ async function authenticate(ctx: AccountContext): Promise<{ access: string; refr
|
|
|
51
43
|
return { access: result.access, refresh: result.refresh, expires: result.expires };
|
|
52
44
|
}
|
|
53
45
|
|
|
46
|
+
/** Resolves and persists identity once; token rotation does not change it. */
|
|
47
|
+
async function stableIdentity(account: Account): Promise<string | undefined> {
|
|
48
|
+
if (account.identity) return account.identity;
|
|
49
|
+
let access = account.access;
|
|
50
|
+
if (!access || (typeof account.expires === "number" && account.expires <= Date.now())) {
|
|
51
|
+
try { access = await ensureAccessToken(account); } catch { return undefined; }
|
|
52
|
+
}
|
|
53
|
+
if (!access) return undefined;
|
|
54
|
+
const identity = await anthropicAccountIdentity(access);
|
|
55
|
+
if (!identity) return undefined;
|
|
56
|
+
|
|
57
|
+
const current = loadAccounts()?.accounts.find((candidate) => candidate.id === account.id);
|
|
58
|
+
if (current && !current.identity) saveAccount({ ...current, identity });
|
|
59
|
+
return identity;
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
export const anthropicAccounts: AccountProvider = {
|
|
55
63
|
id: "anthropic",
|
|
56
64
|
label: "Claude",
|
|
57
65
|
|
|
58
66
|
async list(): Promise<ManagedAccount[]> {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
const accounts = (loadAccounts()?.accounts ?? []).filter((account) => account.type === "oauth");
|
|
68
|
+
const result: ManagedAccount[] = [];
|
|
69
|
+
// Bootstrap is deliberately serial: this is a one-time migration for old
|
|
70
|
+
// rows, and concurrent profile requests can be throttled independently.
|
|
71
|
+
for (const account of accounts) {
|
|
72
|
+
result.push({
|
|
63
73
|
id: account.id,
|
|
64
74
|
label: account.label ?? account.id.slice(0, 8),
|
|
65
75
|
enabled: account.enabled !== false,
|
|
66
76
|
expiresAt: typeof account.expires === "number" ? account.expires : undefined,
|
|
67
|
-
|
|
77
|
+
identity: await stableIdentity(account),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
68
81
|
},
|
|
69
82
|
|
|
83
|
+
identify: anthropicAccountIdentity,
|
|
84
|
+
|
|
70
85
|
async add(ctx, label): Promise<string | undefined> {
|
|
71
86
|
const proceed = await ctx.ui.confirm(
|
|
72
87
|
"Add Claude account",
|
|
@@ -78,6 +93,7 @@ export const anthropicAccounts: AccountProvider = {
|
|
|
78
93
|
if (!result) return undefined;
|
|
79
94
|
|
|
80
95
|
const now = Date.now();
|
|
96
|
+
const identity = await anthropicAccountIdentity(result.access);
|
|
81
97
|
saveAccount({
|
|
82
98
|
id: randomUUID(),
|
|
83
99
|
label,
|
|
@@ -88,6 +104,7 @@ export const anthropicAccounts: AccountProvider = {
|
|
|
88
104
|
expires: result.expires,
|
|
89
105
|
addedAt: now,
|
|
90
106
|
lastRefreshedAt: now,
|
|
107
|
+
identity,
|
|
91
108
|
authLineageId: randomUUID(),
|
|
92
109
|
});
|
|
93
110
|
return label;
|
|
@@ -105,12 +122,16 @@ export const anthropicAccounts: AccountProvider = {
|
|
|
105
122
|
const result = await authenticate(ctx);
|
|
106
123
|
if (!result) return undefined;
|
|
107
124
|
|
|
125
|
+
const identity = await anthropicAccountIdentity(result.access);
|
|
108
126
|
saveAccount({
|
|
109
127
|
...account,
|
|
110
128
|
access: result.access,
|
|
111
129
|
refresh: result.refresh,
|
|
112
130
|
expires: result.expires,
|
|
113
131
|
lastRefreshedAt: Date.now(),
|
|
132
|
+
lastRefreshError: undefined,
|
|
133
|
+
identity,
|
|
134
|
+
quota: undefined,
|
|
114
135
|
authLineageId: randomUUID(),
|
|
115
136
|
});
|
|
116
137
|
return account.label ?? account.id;
|
|
@@ -133,17 +154,16 @@ export const anthropicAccounts: AccountProvider = {
|
|
|
133
154
|
|
|
134
155
|
routing: {
|
|
135
156
|
async get(): Promise<RoutingMode> {
|
|
136
|
-
return
|
|
157
|
+
return getRoutingMode(loadAccounts());
|
|
137
158
|
},
|
|
138
159
|
async set(mode: RoutingMode): Promise<RoutingMode> {
|
|
139
|
-
setRoutingMode(
|
|
160
|
+
setRoutingMode(mode);
|
|
140
161
|
return mode;
|
|
141
162
|
},
|
|
142
163
|
describe(mode: RoutingMode): string {
|
|
143
|
-
return mode === "
|
|
144
|
-
? "
|
|
145
|
-
|
|
146
|
-
: "Main subscription first, then eligible fallbacks.";
|
|
164
|
+
return mode === "quota-aware"
|
|
165
|
+
? "Uses the Claude subscription with the most remaining quota."
|
|
166
|
+
: "Uses Claude subscriptions in order, moving on when one is exhausted.";
|
|
147
167
|
},
|
|
148
168
|
},
|
|
149
169
|
};
|
|
@@ -1,148 +1,181 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import type { OAuthCredential } from "@earendil-works/pi-ai";
|
|
2
|
+
import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { RoutingMode } from "../../../core/accounts/registry.ts";
|
|
5
|
+
import { normalizeRoutingMode, type AccountQuotaState } from "../../../core/accounts/routing.ts";
|
|
6
|
+
import type { PooledOAuthAccount, PooledOAuthStore } from "../../../core/accounts/oauth-pool.ts";
|
|
7
|
+
import { applyCodexQuotaHeaders } from "../../../core/codex/quota.ts";
|
|
4
8
|
import {
|
|
9
|
+
MAIN_ACCOUNT_ID,
|
|
5
10
|
claimsOf,
|
|
6
11
|
loadCodexAccounts,
|
|
7
12
|
saveCodexAccount,
|
|
8
13
|
saveCodexAccounts,
|
|
9
14
|
type CodexAccount,
|
|
10
15
|
} from "../../../core/codex/store.ts";
|
|
16
|
+
import {
|
|
17
|
+
chooseCredential,
|
|
18
|
+
createPooledOAuthAdapter,
|
|
19
|
+
registerPooledOAuthProvider,
|
|
20
|
+
type PooledOAuthProviderSpec,
|
|
21
|
+
} from "./oauth-pool.ts";
|
|
11
22
|
|
|
12
23
|
/**
|
|
13
24
|
* Codex adapter.
|
|
14
25
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
26
|
+
* Codex shares the pooled serving path with every other subscription provider
|
|
27
|
+
* (one `routedAuth`, one bounded refresh, one quota observer). Only the two
|
|
28
|
+
* genuinely provider-specific pieces are supplied here:
|
|
29
|
+
*
|
|
30
|
+
* - storage stays in `codex-accounts.json`, because it carries `accountId`
|
|
31
|
+
* (which the usage bars read) and a rich quota snapshot that the generic
|
|
32
|
+
* pool schema has no room for;
|
|
33
|
+
* - quota arrives through `x-codex-*` response headers rather than the
|
|
34
|
+
* generic `x-ratelimit-*` set, so there is no polling path at all.
|
|
35
|
+
*
|
|
36
|
+
* The ChatGPT account id does NOT need to be threaded through auth: the
|
|
37
|
+
* provider derives it from the access token's JWT claims and sets the
|
|
38
|
+
* `chatgpt-account-id` header itself, so swapping the token is self-consistent.
|
|
20
39
|
*/
|
|
21
40
|
|
|
22
|
-
|
|
23
|
-
const
|
|
41
|
+
function describePlan(account: CodexAccount): string {
|
|
42
|
+
const name = account.label ?? account.id.slice(0, 8);
|
|
43
|
+
return account.plan ? `${name} (${account.plan})` : name;
|
|
44
|
+
}
|
|
24
45
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
/** Routing view of a stored Codex snapshot. */
|
|
47
|
+
function quotaState(quota: CodexAccount["quota"], blockedUntil?: number): AccountQuotaState | undefined {
|
|
48
|
+
const blocked = blockedUntil && blockedUntil > Date.now() ? blockedUntil : undefined;
|
|
49
|
+
if (!quota) {
|
|
50
|
+
return blocked ? { remainingPercent: 0, checkedAt: Date.now(), blockedUntil: blocked } : undefined;
|
|
51
|
+
}
|
|
52
|
+
const windows = [quota.five_hour, quota.seven_day, ...(quota.scoped ?? [])]
|
|
53
|
+
.filter((window) => window && Number.isFinite(window.remainingPercent));
|
|
54
|
+
if (windows.length === 0) {
|
|
55
|
+
return blocked ? { remainingPercent: 0, checkedAt: quota.checkedAt ?? Date.now(), blockedUntil: blocked } : undefined;
|
|
29
56
|
}
|
|
30
57
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
58
|
+
const remainingPercent = Math.min(...windows.map((window) => window!.remainingPercent!));
|
|
59
|
+
const resetTimes = windows
|
|
60
|
+
.filter((window) => (window!.remainingPercent ?? 0) <= 0 && window!.resetsAt)
|
|
61
|
+
.map((window) => Date.parse(window!.resetsAt!))
|
|
62
|
+
.filter(Number.isFinite);
|
|
63
|
+
const resetAt = resetTimes.length > 0 ? Math.min(...resetTimes) : undefined;
|
|
64
|
+
return {
|
|
65
|
+
remainingPercent: blocked ? 0 : remainingPercent,
|
|
66
|
+
resetAt,
|
|
67
|
+
checkedAt: quota.checkedAt ?? Date.now(),
|
|
68
|
+
// An exhausted window with no reset time is already non-viable, so there is
|
|
69
|
+
// no sentinel here: `Infinity` would serialize to null and silently unblock.
|
|
70
|
+
blockedUntil: blocked ?? (remainingPercent <= 0 ? resetAt : undefined),
|
|
71
|
+
};
|
|
35
72
|
}
|
|
36
73
|
|
|
37
|
-
|
|
38
|
-
|
|
74
|
+
/** True when the account has a complete credential and can serve a request. */
|
|
75
|
+
function isRoutable(account: CodexAccount): boolean {
|
|
76
|
+
return typeof account.access === "string"
|
|
77
|
+
&& typeof account.refresh === "string"
|
|
78
|
+
&& typeof account.expires === "number";
|
|
39
79
|
}
|
|
40
80
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// one quota pool, which looks like capacity that does not exist.
|
|
68
|
-
const existing = loadCodexAccounts().accounts.find(
|
|
69
|
-
(a) => claims.accountId && a.accountId === claims.accountId,
|
|
70
|
-
);
|
|
71
|
-
if (existing) {
|
|
72
|
-
throw new Error(
|
|
73
|
-
`That ChatGPT account is already added as “${existing.label ?? existing.id.slice(0, 8)}”. `
|
|
74
|
-
+ "Use the browser's account switcher to sign in as a different account.",
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const now = Date.now();
|
|
79
|
-
saveCodexAccount({
|
|
80
|
-
id: randomUUID(),
|
|
81
|
-
label,
|
|
82
|
-
enabled: true,
|
|
83
|
-
access: tokens.access,
|
|
84
|
-
refresh: tokens.refresh,
|
|
85
|
-
expires: tokens.expires,
|
|
86
|
-
accountId: claims.accountId,
|
|
87
|
-
plan: claims.plan,
|
|
88
|
-
addedAt: now,
|
|
89
|
-
lastRefreshedAt: now,
|
|
90
|
-
});
|
|
91
|
-
return label;
|
|
81
|
+
/**
|
|
82
|
+
* Adapts `codex-accounts.json` to the pooled store contract.
|
|
83
|
+
*
|
|
84
|
+
* `quota` and `identity` are derived views, so they are never written back:
|
|
85
|
+
* the pool would otherwise overwrite the rich snapshot the usage bars read
|
|
86
|
+
* with the flattened routing state.
|
|
87
|
+
*/
|
|
88
|
+
const CODEX_STORE: PooledOAuthStore = {
|
|
89
|
+
load() {
|
|
90
|
+
const storage = loadCodexAccounts();
|
|
91
|
+
return {
|
|
92
|
+
mode: normalizeRoutingMode(storage.routing?.mode),
|
|
93
|
+
accounts: storage.accounts.map((account) => ({
|
|
94
|
+
...account,
|
|
95
|
+
type: "oauth" as const,
|
|
96
|
+
// Blanking access on an incomplete credential keeps it out of routing
|
|
97
|
+
// while still listing it under `/accounts`.
|
|
98
|
+
access: isRoutable(account) ? account.access! : "",
|
|
99
|
+
refresh: account.refresh ?? "",
|
|
100
|
+
expires: account.expires ?? 0,
|
|
101
|
+
label: account.label ?? "",
|
|
102
|
+
identity: account.accountId,
|
|
103
|
+
addedAt: account.addedAt ?? 0,
|
|
104
|
+
quota: quotaState(account.quota, account.blockedUntil),
|
|
105
|
+
})),
|
|
106
|
+
};
|
|
92
107
|
},
|
|
93
108
|
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
);
|
|
98
|
-
if (!account) throw new Error(`Codex account “${accountId}” not found.`);
|
|
99
|
-
|
|
100
|
-
const tokens = await authenticate(ctx);
|
|
101
|
-
if (!tokens) return undefined;
|
|
102
|
-
|
|
103
|
-
const claims = claimsOf(tokens.access);
|
|
109
|
+
saveAccount(account: PooledOAuthAccount) {
|
|
110
|
+
const { quota: _quota, identity: _identity, type: _type, ...rest } = account as PooledOAuthAccount & CodexAccount;
|
|
111
|
+
const claims = claimsOf(rest.access);
|
|
104
112
|
saveCodexAccount({
|
|
105
|
-
...
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
expires: tokens.expires,
|
|
109
|
-
accountId: claims.accountId ?? account.accountId,
|
|
110
|
-
plan: claims.plan ?? account.plan,
|
|
111
|
-
lastRefreshedAt: Date.now(),
|
|
113
|
+
...rest,
|
|
114
|
+
accountId: rest.accountId ?? claims.accountId,
|
|
115
|
+
plan: rest.plan ?? claims.plan,
|
|
112
116
|
});
|
|
113
|
-
return account.label ?? account.id;
|
|
114
117
|
},
|
|
115
118
|
|
|
116
|
-
|
|
119
|
+
saveMode(mode) {
|
|
117
120
|
const storage = loadCodexAccounts();
|
|
118
|
-
|
|
119
|
-
if (!account) throw new Error(`Account “${accountId}” not found.`);
|
|
120
|
-
account.enabled = enabled;
|
|
121
|
+
storage.routing = { ...(storage.routing ?? {}), mode };
|
|
121
122
|
saveCodexAccounts(storage);
|
|
122
123
|
},
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
primaryQuota() {
|
|
125
126
|
const storage = loadCodexAccounts();
|
|
126
|
-
|
|
127
|
-
if (!account) throw new Error(`Account “${accountId}” not found.`);
|
|
128
|
-
account.label = label;
|
|
129
|
-
saveCodexAccounts(storage);
|
|
127
|
+
return quotaState(storage.main?.quota, storage.main?.blockedUntil);
|
|
130
128
|
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Persists a rate-limit block so routing skips the account until it clears. */
|
|
132
|
+
function markCodexRateLimited(accountId: string, headers: Record<string, string>): void {
|
|
133
|
+
const raw = Object.entries(headers).find(([key]) => key.toLowerCase() === "retry-after")?.[1];
|
|
134
|
+
const seconds = Number(raw);
|
|
135
|
+
const blockedUntil = Date.now() + (Number.isFinite(seconds) && seconds >= 0 ? seconds * 1_000 : 60_000);
|
|
136
|
+
const storage = loadCodexAccounts();
|
|
137
|
+
if (accountId === MAIN_ACCOUNT_ID) {
|
|
138
|
+
storage.main = { ...storage.main, blockedUntil };
|
|
139
|
+
saveCodexAccounts(storage);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const account = storage.accounts.find((candidate) => candidate.id === accountId);
|
|
143
|
+
if (account) saveCodexAccount({ ...account, blockedUntil });
|
|
144
|
+
}
|
|
131
145
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
export const CODEX_SPEC: PooledOAuthProviderSpec<"openai-codex-responses"> = {
|
|
147
|
+
id: "openai-codex",
|
|
148
|
+
label: "Codex",
|
|
149
|
+
createProvider: openaiCodexProvider,
|
|
150
|
+
store: CODEX_STORE,
|
|
151
|
+
addPrompt: "Sign in with a DIFFERENT ChatGPT account in the browser. Continue?",
|
|
152
|
+
describeAccount: (account) => describePlan(account as PooledOAuthAccount & CodexAccount),
|
|
153
|
+
// Two entries backed by one ChatGPT account would look like capacity that
|
|
154
|
+
// does not exist, so duplicates are matched on the token's account claim.
|
|
155
|
+
identityOf: (access) => claimsOf(access).accountId,
|
|
156
|
+
recordQuota(accountId, status, headers) {
|
|
157
|
+
// Never throws: swallows its own write failures.
|
|
158
|
+
applyCodexQuotaHeaders(accountId, headers);
|
|
159
|
+
if (status !== 429) return;
|
|
160
|
+
try {
|
|
161
|
+
markCodexRateLimited(accountId, headers);
|
|
162
|
+
} catch {
|
|
163
|
+
// Telemetry only — a failed block write must not fail the response.
|
|
164
|
+
}
|
|
147
165
|
},
|
|
148
166
|
};
|
|
167
|
+
|
|
168
|
+
export const codexAccounts = createPooledOAuthAdapter(CODEX_SPEC);
|
|
169
|
+
|
|
170
|
+
export function codexRoutingMode(value: string | undefined): RoutingMode {
|
|
171
|
+
return normalizeRoutingMode(value);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Retained for callers and tests that select a Codex account directly. */
|
|
175
|
+
export function chooseCodexCredential(primary: OAuthCredential) {
|
|
176
|
+
return chooseCredential(CODEX_SPEC, primary);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function registerCodexProvider(pi: ExtensionAPI): void {
|
|
180
|
+
registerPooledOAuthProvider(pi, CODEX_SPEC);
|
|
181
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { kimiCodingProvider } from "@earendil-works/pi-ai/providers/kimi-coding";
|
|
2
|
+
import { xaiProvider } from "@earendil-works/pi-ai/providers/xai";
|
|
3
|
+
import { createPooledOAuthAdapter, type PooledOAuthProviderSpec } from "./oauth-pool.ts";
|
|
4
|
+
|
|
5
|
+
export const KIMI_SPEC: PooledOAuthProviderSpec<"anthropic-messages"> = {
|
|
6
|
+
id: "kimi-coding",
|
|
7
|
+
label: "Kimi",
|
|
8
|
+
createProvider: kimiCodingProvider,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const XAI_SPEC: PooledOAuthProviderSpec<"openai-responses"> = {
|
|
12
|
+
id: "xai",
|
|
13
|
+
label: "Grok",
|
|
14
|
+
createProvider: xaiProvider,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const kimiAccounts = createPooledOAuthAdapter(KIMI_SPEC);
|
|
18
|
+
export const xaiAccounts = createPooledOAuthAdapter(XAI_SPEC);
|