@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.
@@ -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
- function modelSpec(modelId: string): string {
266
- const provider = modelId.startsWith("claude-") ? "pp-flant-anthropic" : "pp-flant-openai";
267
- return `${provider}/${modelId}`;
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
- updateRegistryFromAvailableModels([
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) return;
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
  }