@ilya-lesikov/pi-pi 0.6.0 → 0.7.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/3p/pi-ask-user/index.ts +1 -1
- package/extensions/orchestrator/config.ts +3 -2
- package/extensions/orchestrator/custom-footer.ts +5 -2
- package/extensions/orchestrator/doctor.test.ts +3 -1
- package/extensions/orchestrator/doctor.ts +40 -2
- package/extensions/orchestrator/event-handlers.ts +47 -6
- package/extensions/orchestrator/flant-infra.test.ts +287 -0
- package/extensions/orchestrator/flant-infra.ts +316 -44
- package/extensions/orchestrator/integration.test.ts +205 -6
- package/extensions/orchestrator/model-registry.test.ts +76 -12
- package/extensions/orchestrator/model-registry.ts +48 -32
- package/extensions/orchestrator/orchestrator.ts +13 -2
- package/extensions/orchestrator/phases/review.test.ts +54 -0
- package/extensions/orchestrator/phases/review.ts +54 -12
- package/extensions/orchestrator/pp-menu.test.ts +74 -1
- package/extensions/orchestrator/pp-menu.ts +199 -41
- package/extensions/orchestrator/state.ts +41 -20
- package/extensions/orchestrator/test-helpers.ts +14 -2
- package/extensions/orchestrator/usage-tracker.test.ts +131 -3
- package/extensions/orchestrator/usage-tracker.ts +74 -11
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import lockfile from "proper-lockfile";
|
|
5
|
+
import { refreshAnthropicToken } from "@earendil-works/pi-ai/oauth";
|
|
5
6
|
import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
6
7
|
import type { PiPiConfig } from "./config.js";
|
|
7
8
|
import { updateRegistryFromAvailableModels } from "./model-registry.js";
|
|
@@ -28,6 +29,12 @@ export interface FlantSettings {
|
|
|
28
29
|
lastUpdated: string | null;
|
|
29
30
|
cachedFlantModels: string[] | null;
|
|
30
31
|
cachedOpenRouterData: Record<string, OpenRouterModelData> | null;
|
|
32
|
+
/**
|
|
33
|
+
* When true, additionally register the `pp-flant-anthropic-sub` provider,
|
|
34
|
+
* which routes Claude requests through the gateway using the user's personal
|
|
35
|
+
* Claude OAuth token (billed against their personal Claude subscription).
|
|
36
|
+
*/
|
|
37
|
+
subscription: boolean;
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
const GEMINI_MAP: Record<string, string> = {
|
|
@@ -56,11 +63,153 @@ const DEFAULT_SETTINGS: FlantSettings = {
|
|
|
56
63
|
lastUpdated: null,
|
|
57
64
|
cachedFlantModels: null,
|
|
58
65
|
cachedOpenRouterData: null,
|
|
66
|
+
subscription: false,
|
|
59
67
|
};
|
|
60
68
|
|
|
69
|
+
/** Provider name for the personal-subscription Claude routing. */
|
|
70
|
+
export const SUB_PROVIDER = "pp-flant-anthropic-sub";
|
|
71
|
+
|
|
72
|
+
/** Prefix the gateway expects for personal-subscription Claude models. */
|
|
73
|
+
export const SUB_MODEL_PREFIX = "sub/";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read the Claude OAuth access token persisted by pi's built-in `anthropic`
|
|
77
|
+
* OAuth provider. Returns null when absent or expired. The gateway uses this
|
|
78
|
+
* token (forwarded as `Authorization: Bearer ...`) to bill the user's personal
|
|
79
|
+
* Claude subscription; pi-ai automatically adds the Claude Code identity
|
|
80
|
+
* headers because the token has the `sk-ant-oat` prefix.
|
|
81
|
+
*/
|
|
82
|
+
export function readClaudeOAuthToken(): string | null {
|
|
83
|
+
const authPath = join(resolveAgentDir(), "auth.json");
|
|
84
|
+
if (!existsSync(authPath)) return null;
|
|
85
|
+
try {
|
|
86
|
+
const raw = JSON.parse(readFileSync(authPath, "utf-8")) as {
|
|
87
|
+
anthropic?: { access?: unknown; expires?: unknown };
|
|
88
|
+
};
|
|
89
|
+
const anthropic = raw.anthropic;
|
|
90
|
+
if (!anthropic || typeof anthropic.access !== "string" || !anthropic.access) return null;
|
|
91
|
+
if (typeof anthropic.expires === "number" && anthropic.expires <= Date.now()) return null;
|
|
92
|
+
return anthropic.access;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface AnthropicOAuthCreds {
|
|
99
|
+
type?: unknown;
|
|
100
|
+
access?: unknown;
|
|
101
|
+
refresh?: unknown;
|
|
102
|
+
expires?: unknown;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Ensure the persisted Claude OAuth access token is fresh, refreshing it via
|
|
107
|
+
* the stored refresh token when it has expired (or is within its safety
|
|
108
|
+
* margin). The refreshed credentials are written back to `auth.json` under the
|
|
109
|
+
* `anthropic` provider id in pi's own `{ type: "oauth", ... }` format, so both
|
|
110
|
+
* this extension and pi's built-in `anthropic` provider observe the new token.
|
|
111
|
+
*
|
|
112
|
+
* Unlike readClaudeOAuthToken, this is async (a refresh performs a network
|
|
113
|
+
* call) and returns the valid access token, or null when no usable
|
|
114
|
+
* credentials exist / a refresh fails. Async entry points call this before the
|
|
115
|
+
* synchronous readClaudeOAuthToken so downstream reads see a fresh token.
|
|
116
|
+
*/
|
|
117
|
+
export async function refreshClaudeOAuthToken(): Promise<string | null> {
|
|
118
|
+
const log = getLogger();
|
|
119
|
+
const authPath = join(resolveAgentDir(), "auth.json");
|
|
120
|
+
if (!existsSync(authPath)) return null;
|
|
121
|
+
|
|
122
|
+
let anthropic: AnthropicOAuthCreds | undefined;
|
|
123
|
+
try {
|
|
124
|
+
const raw = JSON.parse(readFileSync(authPath, "utf-8")) as { anthropic?: AnthropicOAuthCreds };
|
|
125
|
+
anthropic = raw.anthropic;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
if (!anthropic || typeof anthropic.access !== "string" || !anthropic.access) return null;
|
|
130
|
+
|
|
131
|
+
const expires = typeof anthropic.expires === "number" ? anthropic.expires : 0;
|
|
132
|
+
if (expires > Date.now()) return anthropic.access;
|
|
133
|
+
|
|
134
|
+
// Expired (or no expiry recorded): try to refresh.
|
|
135
|
+
if (typeof anthropic.refresh !== "string" || !anthropic.refresh) {
|
|
136
|
+
log.debug({ s: "flant" }, "claude oauth token expired and no refresh token available");
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let refreshed: { refresh: string; access: string; expires: number };
|
|
141
|
+
try {
|
|
142
|
+
refreshed = await refreshAnthropicToken(anthropic.refresh);
|
|
143
|
+
} catch (err: any) {
|
|
144
|
+
log.debug({ s: "flant", err: err?.message }, "claude oauth token refresh failed");
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Persist refreshed credentials back under the `anthropic` provider id, using
|
|
149
|
+
// pi's { type: "oauth", ... } shape and the same file lock pi uses.
|
|
150
|
+
try {
|
|
151
|
+
const authDir = resolveAgentDir();
|
|
152
|
+
if (!existsSync(authDir)) mkdirSync(authDir, { recursive: true });
|
|
153
|
+
if (!existsSync(authPath)) writeFileSync(authPath, "{}\n", "utf-8");
|
|
154
|
+
const release = lockfile.lockSync(authPath, { stale: 10000 });
|
|
155
|
+
try {
|
|
156
|
+
let current: Record<string, unknown> = {};
|
|
157
|
+
try {
|
|
158
|
+
current = JSON.parse(readFileSync(authPath, "utf-8")) as Record<string, unknown>;
|
|
159
|
+
} catch {
|
|
160
|
+
current = {};
|
|
161
|
+
}
|
|
162
|
+
const existing = (current.anthropic && typeof current.anthropic === "object")
|
|
163
|
+
? current.anthropic as Record<string, unknown>
|
|
164
|
+
: {};
|
|
165
|
+
// Another instance may have refreshed while we were waiting for the lock.
|
|
166
|
+
const existingExpires = typeof existing.expires === "number" ? existing.expires : 0;
|
|
167
|
+
if (existingExpires > Date.now() && typeof existing.access === "string" && existing.access) {
|
|
168
|
+
return existing.access;
|
|
169
|
+
}
|
|
170
|
+
current.anthropic = {
|
|
171
|
+
...existing,
|
|
172
|
+
type: "oauth",
|
|
173
|
+
access: refreshed.access,
|
|
174
|
+
refresh: refreshed.refresh,
|
|
175
|
+
expires: refreshed.expires,
|
|
176
|
+
};
|
|
177
|
+
writeFileSync(authPath, JSON.stringify(current, null, 2) + "\n", "utf-8");
|
|
178
|
+
} finally {
|
|
179
|
+
release();
|
|
180
|
+
}
|
|
181
|
+
} catch (err: any) {
|
|
182
|
+
// If persistence fails we still return the freshly minted token so the
|
|
183
|
+
// current run can proceed; the next run will refresh again.
|
|
184
|
+
log.debug({ s: "flant", err: err?.message }, "failed to persist refreshed claude oauth token");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
log.debug({ s: "flant" }, "refreshed claude oauth token");
|
|
188
|
+
return refreshed.access;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Resolve the gateway API key (LLM_API_KEY preferred, FLANT_API_KEY fallback). */
|
|
192
|
+
export function readGatewayApiKey(): string | null {
|
|
193
|
+
return process.env.LLM_API_KEY ?? process.env.FLANT_API_KEY ?? null;
|
|
194
|
+
}
|
|
195
|
+
|
|
61
196
|
let piRef: ExtensionAPI | null = null;
|
|
62
197
|
let generatedFlantConfig: Partial<PiPiConfig> | null = null;
|
|
63
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Inputs needed to (re)register the personal-subscription Claude provider.
|
|
201
|
+
* Cached at the last registerFlantProviders call so refreshSubProvider can
|
|
202
|
+
* rebuild the provider with a freshly-read OAuth token without re-discovering
|
|
203
|
+
* models. Null when subscription routing is not active.
|
|
204
|
+
*/
|
|
205
|
+
interface SubProviderContext {
|
|
206
|
+
anthropicModels: string[];
|
|
207
|
+
metadata: Record<string, OpenRouterModelData>;
|
|
208
|
+
}
|
|
209
|
+
let subProviderContext: SubProviderContext | null = null;
|
|
210
|
+
/** The OAuth access token the sub provider was last registered with. */
|
|
211
|
+
let lastSubToken: string | null = null;
|
|
212
|
+
|
|
64
213
|
export function setPI(pi: ExtensionAPI): void {
|
|
65
214
|
piRef = pi;
|
|
66
215
|
}
|
|
@@ -74,6 +223,7 @@ export function unregisterFlantProviders(pi?: ExtensionAPI): void {
|
|
|
74
223
|
if (!api) return;
|
|
75
224
|
api.unregisterProvider("pp-flant-anthropic");
|
|
76
225
|
api.unregisterProvider("pp-flant-openai");
|
|
226
|
+
api.unregisterProvider(SUB_PROVIDER);
|
|
77
227
|
}
|
|
78
228
|
|
|
79
229
|
function ensureSettingsDir(): void {
|
|
@@ -99,6 +249,7 @@ function normalizeSettings(raw: unknown): FlantSettings {
|
|
|
99
249
|
enabled: !!value.enabled,
|
|
100
250
|
autoUpdate: value.autoUpdate === undefined ? true : !!value.autoUpdate,
|
|
101
251
|
cacheTTLDays,
|
|
252
|
+
subscription: !!value.subscription,
|
|
102
253
|
lastUpdated: typeof value.lastUpdated === "string" ? value.lastUpdated : null,
|
|
103
254
|
cachedFlantModels: Array.isArray(value.cachedFlantModels)
|
|
104
255
|
? value.cachedFlantModels.filter((m): m is string => typeof m === "string")
|
|
@@ -262,9 +413,24 @@ export async function fetchOpenRouterMetadata(modelIds: string[]): Promise<Recor
|
|
|
262
413
|
return out;
|
|
263
414
|
}
|
|
264
415
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Returns true when the personal-subscription Claude path is fully usable:
|
|
418
|
+
* the setting is enabled AND both credentials (Claude OAuth token + gateway
|
|
419
|
+
* key) are present. Mirrors the gate in registerFlantProviders so we never
|
|
420
|
+
* generate `sub/` role assignments that cannot resolve to a real provider.
|
|
421
|
+
*/
|
|
422
|
+
export function isSubscriptionActive(settings?: FlantSettings): boolean {
|
|
423
|
+
const s = settings ?? loadFlantSettings();
|
|
424
|
+
return s.subscription && !!readClaudeOAuthToken() && !!readGatewayApiKey();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function modelSpec(modelId: string, subscriptionActive = false): string {
|
|
428
|
+
if (modelId.startsWith("claude-")) {
|
|
429
|
+
return subscriptionActive
|
|
430
|
+
? `${SUB_PROVIDER}/${SUB_MODEL_PREFIX}${modelId}`
|
|
431
|
+
: `pp-flant-anthropic/${modelId}`;
|
|
432
|
+
}
|
|
433
|
+
return `pp-flant-openai/${modelId}`;
|
|
268
434
|
}
|
|
269
435
|
|
|
270
436
|
function buildProviderModelConfig(
|
|
@@ -289,16 +455,21 @@ function buildProviderModelConfig(
|
|
|
289
455
|
};
|
|
290
456
|
}
|
|
291
457
|
|
|
458
|
+
export interface RegisterFlantOptions {
|
|
459
|
+
/** Whether to also register the personal-subscription provider. Defaults to loadFlantSettings().subscription. */
|
|
460
|
+
subscription?: boolean;
|
|
461
|
+
}
|
|
462
|
+
|
|
292
463
|
export function registerFlantProviders(
|
|
293
464
|
pi: ExtensionAPI,
|
|
294
465
|
models: string[],
|
|
295
466
|
metadata: Record<string, OpenRouterModelData>,
|
|
467
|
+
options: RegisterFlantOptions = {},
|
|
296
468
|
): void {
|
|
297
469
|
const log = getLogger();
|
|
298
470
|
const uniqueModels = [...new Set(models)];
|
|
299
471
|
const anthropicModels = uniqueModels.filter((m) => m.startsWith("claude-"));
|
|
300
472
|
const openaiModels = uniqueModels.filter((m) => !m.startsWith("claude-"));
|
|
301
|
-
log.debug({ s: "flant", total: uniqueModels.length, anthropic: anthropicModels.length, openai: openaiModels.length }, "registering flant providers");
|
|
302
473
|
|
|
303
474
|
unregisterFlantProviders(pi);
|
|
304
475
|
|
|
@@ -316,10 +487,97 @@ export function registerFlantProviders(
|
|
|
316
487
|
models: openaiModels.map((m) => buildProviderModelConfig(m, metadata)),
|
|
317
488
|
});
|
|
318
489
|
|
|
319
|
-
|
|
490
|
+
const availableSpecs = [
|
|
320
491
|
...anthropicModels.map((id) => `pp-flant-anthropic/${id}`),
|
|
321
492
|
...openaiModels.map((id) => `pp-flant-openai/${id}`),
|
|
322
|
-
]
|
|
493
|
+
];
|
|
494
|
+
|
|
495
|
+
const subscription = options.subscription ?? loadFlantSettings().subscription;
|
|
496
|
+
let subModels: string[] = [];
|
|
497
|
+
if (subscription) {
|
|
498
|
+
// Remember the models/metadata so refreshSubProvider can rebuild the
|
|
499
|
+
// provider with a fresh OAuth token on each turn (see below).
|
|
500
|
+
subProviderContext = { anthropicModels, metadata };
|
|
501
|
+
subModels = registerSubProvider(pi, anthropicModels, metadata);
|
|
502
|
+
availableSpecs.push(...subModels.map((id) => `${SUB_PROVIDER}/${id}`));
|
|
503
|
+
} else {
|
|
504
|
+
subProviderContext = null;
|
|
505
|
+
lastSubToken = null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
log.debug({ s: "flant", total: uniqueModels.length, anthropic: anthropicModels.length, openai: openaiModels.length, sub: subModels.length }, "registering flant providers");
|
|
509
|
+
|
|
510
|
+
updateRegistryFromAvailableModels(availableSpecs);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Register (or re-register) the personal-subscription Claude provider using the
|
|
515
|
+
* OAuth access token currently persisted in auth.json. Returns the list of
|
|
516
|
+
* `sub/`-prefixed model ids registered (empty when credentials are missing).
|
|
517
|
+
*
|
|
518
|
+
* The provider is registered with a literal `apiKey` (the resolved token), so
|
|
519
|
+
* the value is a static snapshot for the lifetime of the registration. Because
|
|
520
|
+
* the OAuth token expires within a few hours, refreshSubProvider must be called
|
|
521
|
+
* periodically (on each turn) to rebuild the provider with a fresh token;
|
|
522
|
+
* otherwise long-lived sessions send a stale token and the gateway returns 401.
|
|
523
|
+
*/
|
|
524
|
+
function registerSubProvider(
|
|
525
|
+
pi: ExtensionAPI,
|
|
526
|
+
anthropicModels: string[],
|
|
527
|
+
metadata: Record<string, OpenRouterModelData>,
|
|
528
|
+
): string[] {
|
|
529
|
+
const log = getLogger();
|
|
530
|
+
const oauthToken = readClaudeOAuthToken();
|
|
531
|
+
const gatewayKey = readGatewayApiKey();
|
|
532
|
+
if (!oauthToken || !gatewayKey) {
|
|
533
|
+
log.debug({ s: "flant", hasOAuth: !!oauthToken, hasGatewayKey: !!gatewayKey }, "subscription enabled but credentials missing; skipping sub provider");
|
|
534
|
+
lastSubToken = null;
|
|
535
|
+
return [];
|
|
536
|
+
}
|
|
537
|
+
pi.registerProvider(SUB_PROVIDER, {
|
|
538
|
+
name: "Flant (personal Claude subscription)",
|
|
539
|
+
api: "anthropic-messages",
|
|
540
|
+
baseUrl: "https://llm-api.flant.ru",
|
|
541
|
+
// The OAuth token (sk-ant-oat...) triggers pi-ai's Claude Code identity
|
|
542
|
+
// headers and is forwarded as `Authorization: Bearer ...`.
|
|
543
|
+
apiKey: oauthToken,
|
|
544
|
+
// Gateway key travels in a side header so it does not clobber the OAuth auth.
|
|
545
|
+
headers: { "x-litellm-api-key": `Bearer ${gatewayKey}` },
|
|
546
|
+
// Model id carries the `sub/` prefix the gateway expects, while pricing/
|
|
547
|
+
// metadata is looked up by the bare claude-* id.
|
|
548
|
+
models: anthropicModels.map((m) => {
|
|
549
|
+
const cfg = buildProviderModelConfig(m, metadata);
|
|
550
|
+
return { ...cfg, id: `${SUB_MODEL_PREFIX}${m}` };
|
|
551
|
+
}),
|
|
552
|
+
});
|
|
553
|
+
lastSubToken = oauthToken;
|
|
554
|
+
return anthropicModels.map((m) => `${SUB_MODEL_PREFIX}${m}`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Ensure the personal-subscription Claude provider is registered with a
|
|
559
|
+
* non-expired OAuth token. Refreshes the token (persisting it to auth.json when
|
|
560
|
+
* needed) and, when it changed since the last registration, re-registers the
|
|
561
|
+
* provider so subsequent LLM calls pick up the fresh token.
|
|
562
|
+
*
|
|
563
|
+
* Called on each turn for the root session. No-op when subscription routing is
|
|
564
|
+
* not active. Cheap when the token is unchanged (only a token read + compare).
|
|
565
|
+
*/
|
|
566
|
+
export async function refreshSubProvider(pi?: ExtensionAPI): Promise<void> {
|
|
567
|
+
const ctx = subProviderContext;
|
|
568
|
+
if (!ctx) return;
|
|
569
|
+
const api = pi ?? piRef;
|
|
570
|
+
if (!api) return;
|
|
571
|
+
|
|
572
|
+
await refreshClaudeOAuthToken();
|
|
573
|
+
const token = readClaudeOAuthToken();
|
|
574
|
+
// Only re-register when the token actually changed; registerProvider takes
|
|
575
|
+
// effect immediately, so re-registering every turn would be wasteful churn.
|
|
576
|
+
if (token && token === lastSubToken) return;
|
|
577
|
+
|
|
578
|
+
const log = getLogger();
|
|
579
|
+
registerSubProvider(api, ctx.anthropicModels, ctx.metadata);
|
|
580
|
+
log.debug({ s: "flant", changed: token !== lastSubToken }, "refreshed sub provider oauth token");
|
|
323
581
|
}
|
|
324
582
|
|
|
325
583
|
function pickLatest(models: string[]): string | null {
|
|
@@ -339,18 +597,19 @@ function pickCheapestFastModel(models: string[]): string | null {
|
|
|
339
597
|
return pickLatest(models.filter((m) => /^claude-haiku-/.test(m)));
|
|
340
598
|
}
|
|
341
599
|
|
|
342
|
-
function makeVariant(modelId: string | null, fallbackModelId: string): { enabled: boolean; model: string; thinking: string } {
|
|
343
|
-
if (!modelId) return { enabled: false, model: modelSpec(fallbackModelId), thinking: "high" };
|
|
344
|
-
return { enabled: true, model: modelSpec(modelId), thinking: "high" };
|
|
600
|
+
function makeVariant(modelId: string | null, fallbackModelId: string, sub = false): { enabled: boolean; model: string; thinking: string } {
|
|
601
|
+
if (!modelId) return { enabled: false, model: modelSpec(fallbackModelId, sub), thinking: "high" };
|
|
602
|
+
return { enabled: true, model: modelSpec(modelId, sub), thinking: "high" };
|
|
345
603
|
}
|
|
346
604
|
|
|
347
605
|
function makeVariantWithThinking(
|
|
348
606
|
modelId: string | null,
|
|
349
607
|
fallbackModelId: string,
|
|
350
608
|
thinking: string,
|
|
609
|
+
sub = false,
|
|
351
610
|
): { enabled: boolean; model: string; thinking: string } {
|
|
352
|
-
if (!modelId) return { enabled: false, model: modelSpec(fallbackModelId), thinking };
|
|
353
|
-
return { enabled: true, model: modelSpec(modelId), thinking };
|
|
611
|
+
if (!modelId) return { enabled: false, model: modelSpec(fallbackModelId, sub), thinking };
|
|
612
|
+
return { enabled: true, model: modelSpec(modelId, sub), thinking };
|
|
354
613
|
}
|
|
355
614
|
|
|
356
615
|
function buildPresetGroup(
|
|
@@ -360,9 +619,10 @@ function buildPresetGroup(
|
|
|
360
619
|
return { default: defaultPreset, presets };
|
|
361
620
|
}
|
|
362
621
|
|
|
363
|
-
export function generateFlantConfig(models: string[]): Partial<PiPiConfig> {
|
|
622
|
+
export function generateFlantConfig(models: string[], subscriptionActive = false): Partial<PiPiConfig> {
|
|
364
623
|
const uniqueModels = [...new Set(models)];
|
|
365
624
|
if (uniqueModels.length === 0) return {};
|
|
625
|
+
const sub = subscriptionActive;
|
|
366
626
|
|
|
367
627
|
const latestOpus = pickLatest(uniqueModels.filter((m) => /^claude-opus-/.test(m)));
|
|
368
628
|
const latestClaude = pickLatest(uniqueModels.filter((m) => /^claude-/.test(m)));
|
|
@@ -383,73 +643,74 @@ export function generateFlantConfig(models: string[]): Partial<PiPiConfig> {
|
|
|
383
643
|
return {
|
|
384
644
|
agents: {
|
|
385
645
|
orchestrators: {
|
|
386
|
-
implement: { model: modelSpec(implementModel), thinking: "high" },
|
|
387
|
-
plan: { model: modelSpec(implementModel), thinking: "high" },
|
|
388
|
-
debug: { model: modelSpec(debugModel), thinking: "high" },
|
|
389
|
-
brainstorm: { model: modelSpec(brainstormModel), thinking: "high" },
|
|
390
|
-
review: { model: modelSpec(implementModel), thinking: "high" },
|
|
646
|
+
implement: { model: modelSpec(implementModel, sub), thinking: "high" },
|
|
647
|
+
plan: { model: modelSpec(implementModel, sub), thinking: "high" },
|
|
648
|
+
debug: { model: modelSpec(debugModel, sub), thinking: "high" },
|
|
649
|
+
brainstorm: { model: modelSpec(brainstormModel, sub), thinking: "high" },
|
|
650
|
+
review: { model: modelSpec(implementModel, sub), thinking: "high" },
|
|
651
|
+
quick: { model: modelSpec(implementModel, sub), thinking: "high" },
|
|
391
652
|
},
|
|
392
653
|
subagents: {
|
|
393
654
|
simple: {
|
|
394
|
-
explore: { model: modelSpec(fastModel), thinking: "low" },
|
|
395
|
-
librarian: { model: modelSpec(fastModel), thinking: "medium" },
|
|
396
|
-
task: { model: modelSpec(taskModel), thinking: "medium" },
|
|
655
|
+
explore: { model: modelSpec(fastModel, sub), thinking: "low" },
|
|
656
|
+
librarian: { model: modelSpec(fastModel, sub), thinking: "medium" },
|
|
657
|
+
task: { model: modelSpec(taskModel, sub), thinking: "medium" },
|
|
397
658
|
},
|
|
398
659
|
presetGroups: {
|
|
399
660
|
planners: buildPresetGroup({
|
|
400
661
|
regular: {
|
|
401
662
|
agents: {
|
|
402
|
-
opus: makeVariant(latestOpus, fallback),
|
|
403
|
-
gpt: makeVariant(latestGpt, fallback),
|
|
404
|
-
gemini: makeVariant(latestGeminiPro, fallback),
|
|
663
|
+
opus: makeVariant(latestOpus, fallback, sub),
|
|
664
|
+
gpt: makeVariant(latestGpt, fallback, sub),
|
|
665
|
+
gemini: makeVariant(latestGeminiPro, fallback, sub),
|
|
405
666
|
},
|
|
406
667
|
},
|
|
407
668
|
}),
|
|
408
669
|
planReviewers: buildPresetGroup({
|
|
409
670
|
regular: {
|
|
410
671
|
agents: {
|
|
411
|
-
opus: makeVariant(latestOpus, fallback),
|
|
412
|
-
gpt: makeVariant(latestGpt, fallback),
|
|
413
|
-
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh"),
|
|
672
|
+
opus: makeVariant(latestOpus, fallback, sub),
|
|
673
|
+
gpt: makeVariant(latestGpt, fallback, sub),
|
|
674
|
+
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh", sub),
|
|
414
675
|
},
|
|
415
676
|
},
|
|
416
677
|
deep: {
|
|
417
678
|
agents: {
|
|
418
|
-
opus: makeVariantWithThinking(latestOpus, fallback, "xhigh"),
|
|
419
|
-
gpt: makeVariantWithThinking(latestGpt, fallback, "xhigh"),
|
|
420
|
-
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh"),
|
|
679
|
+
opus: makeVariantWithThinking(latestOpus, fallback, "xhigh", sub),
|
|
680
|
+
gpt: makeVariantWithThinking(latestGpt, fallback, "xhigh", sub),
|
|
681
|
+
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh", sub),
|
|
421
682
|
},
|
|
422
683
|
},
|
|
423
684
|
}),
|
|
424
685
|
codeReviewers: buildPresetGroup({
|
|
425
686
|
regular: {
|
|
426
687
|
agents: {
|
|
427
|
-
opus: makeVariant(latestOpus, fallback),
|
|
428
|
-
gpt: makeVariant(latestGpt, fallback),
|
|
429
|
-
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh"),
|
|
688
|
+
opus: makeVariant(latestOpus, fallback, sub),
|
|
689
|
+
gpt: makeVariant(latestGpt, fallback, sub),
|
|
690
|
+
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh", sub),
|
|
430
691
|
},
|
|
431
692
|
},
|
|
432
693
|
deep: {
|
|
433
694
|
agents: {
|
|
434
|
-
opus: makeVariantWithThinking(latestOpus, fallback, "xhigh"),
|
|
435
|
-
gpt: makeVariantWithThinking(latestGpt, fallback, "xhigh"),
|
|
436
|
-
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh"),
|
|
695
|
+
opus: makeVariantWithThinking(latestOpus, fallback, "xhigh", sub),
|
|
696
|
+
gpt: makeVariantWithThinking(latestGpt, fallback, "xhigh", sub),
|
|
697
|
+
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh", sub),
|
|
437
698
|
},
|
|
438
699
|
},
|
|
439
700
|
}),
|
|
440
701
|
brainstormReviewers: buildPresetGroup({
|
|
441
702
|
regular: {
|
|
442
703
|
agents: {
|
|
443
|
-
opus: makeVariant(latestOpus, fallback),
|
|
444
|
-
gpt: makeVariant(latestGpt, fallback),
|
|
445
|
-
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh"),
|
|
704
|
+
opus: makeVariant(latestOpus, fallback, sub),
|
|
705
|
+
gpt: makeVariant(latestGpt, fallback, sub),
|
|
706
|
+
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh", sub),
|
|
446
707
|
},
|
|
447
708
|
},
|
|
448
709
|
deep: {
|
|
449
710
|
agents: {
|
|
450
|
-
opus: makeVariantWithThinking(latestOpus, fallback, "xhigh"),
|
|
451
|
-
gpt: makeVariantWithThinking(latestGpt, fallback, "xhigh"),
|
|
452
|
-
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh"),
|
|
711
|
+
opus: makeVariantWithThinking(latestOpus, fallback, "xhigh", sub),
|
|
712
|
+
gpt: makeVariantWithThinking(latestGpt, fallback, "xhigh", sub),
|
|
713
|
+
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh", sub),
|
|
453
714
|
},
|
|
454
715
|
},
|
|
455
716
|
}),
|
|
@@ -479,6 +740,12 @@ export async function updateFlantInfra(
|
|
|
479
740
|
setPI(pi);
|
|
480
741
|
const settings = loadFlantSettings();
|
|
481
742
|
|
|
743
|
+
// Refresh the personal-subscription Claude OAuth token before (re)registering
|
|
744
|
+
// providers so the sub provider is built with a valid, non-expired token.
|
|
745
|
+
if (settings.subscription) {
|
|
746
|
+
await refreshClaudeOAuthToken();
|
|
747
|
+
}
|
|
748
|
+
|
|
482
749
|
let models = isCacheValid(settings) ? settings.cachedFlantModels : null;
|
|
483
750
|
let metadata = isCacheValid(settings) ? settings.cachedOpenRouterData : null;
|
|
484
751
|
let refreshed = false;
|
|
@@ -522,7 +789,7 @@ export async function updateFlantInfra(
|
|
|
522
789
|
|
|
523
790
|
try {
|
|
524
791
|
registerFlantProviders(pi, models, metadata);
|
|
525
|
-
generatedFlantConfig = generateFlantConfig(models);
|
|
792
|
+
generatedFlantConfig = generateFlantConfig(models, isSubscriptionActive(settings));
|
|
526
793
|
if (!refreshed && settings.cachedFlantModels && settings.cachedOpenRouterData && !settings.lastUpdated) {
|
|
527
794
|
settings.lastUpdated = new Date().toISOString();
|
|
528
795
|
saveFlantSettings(settings);
|
|
@@ -544,7 +811,7 @@ export function initFlantSync(pi: ExtensionAPI): void {
|
|
|
544
811
|
}
|
|
545
812
|
if (settings.cachedFlantModels && settings.cachedOpenRouterData) {
|
|
546
813
|
registerFlantProviders(pi, settings.cachedFlantModels, settings.cachedOpenRouterData);
|
|
547
|
-
generatedFlantConfig = generateFlantConfig(settings.cachedFlantModels);
|
|
814
|
+
generatedFlantConfig = generateFlantConfig(settings.cachedFlantModels, isSubscriptionActive(settings));
|
|
548
815
|
}
|
|
549
816
|
}
|
|
550
817
|
|
|
@@ -555,6 +822,11 @@ export async function initFlantOnStartup(pi: ExtensionAPI): Promise<void> {
|
|
|
555
822
|
generatedFlantConfig = null;
|
|
556
823
|
return;
|
|
557
824
|
}
|
|
558
|
-
if (!settings.autoUpdate)
|
|
825
|
+
if (!settings.autoUpdate) {
|
|
826
|
+
// updateFlantInfra (which refreshes the token) is skipped when auto-update
|
|
827
|
+
// is off, but any registered sub provider still needs a fresh token.
|
|
828
|
+
if (settings.subscription) await refreshClaudeOAuthToken();
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
559
831
|
await updateFlantInfra(pi);
|
|
560
832
|
}
|