@monotykamary/pi-better-grok 0.3.4 → 0.3.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-better-grok
2
2
 
3
- Better Grok/xAI for [pi](https://pi.dev) — mirrors the [pi-better-openai](https://github.com/monotykamary/pi-better-openai) UX for SuperGrok subscribers: fast mode, subscription usage in the footer, footer polish, and a settings picker. Until pi-core ships it, Grok 4.7 is layered onto the existing `xai` (and sibling Grok) providers as a custom model.
3
+ Better Grok/xAI for [pi](https://pi.dev) — mirrors the [pi-better-openai](https://github.com/monotykamary/pi-better-openai) UX for SuperGrok subscribers: fast mode, subscription usage in the footer, footer polish, and a settings picker. Until pi-core ships it, Grok 4.7 is layered onto the builtin `xai` provider via `~/.pi/agent/models.json` so session restore and scoped-model matching can see it at startup.
4
4
 
5
5
  ## Install
6
6
 
package/index.ts CHANGED
@@ -73,7 +73,14 @@ import {
73
73
  } from "./src/grok-auth.ts";
74
74
  import { currentModelKey, FastController, modelList, supportsFast } from "./src/fast-controller.ts";
75
75
  import { isGrokSubscriptionModel, UsageController } from "./src/usage-controller.ts";
76
- import { registerGrok47OnProviders } from "./src/xai-models.ts";
76
+ import { piAgentDir } from "./src/paths.ts";
77
+ import {
78
+ GROK_47_ID,
79
+ ensureGrok47InModelsJsonFile,
80
+ lastSessionModel,
81
+ registerGrok47OnProviders,
82
+ shouldRestoreGrok47,
83
+ } from "./src/xai-models.ts";
77
84
  import { sep } from "node:path";
78
85
 
79
86
  // pi-core's getSettingsListTheme pulls the host module graph into this
@@ -176,6 +183,17 @@ export function abbreviateHomePath(
176
183
  }
177
184
 
178
185
  export default function betterGrok(pi: ExtensionAPI): void {
186
+ // models.json is the only catalog layer that exists when pi restores the
187
+ // last model, before session_start. registerProvider is too late and replaces
188
+ // the xAI list, which drops grok-4.7 back out of scoped-model matching.
189
+ if (!process.env.VITEST) {
190
+ try {
191
+ ensureGrok47InModelsJsonFile(piAgentDir());
192
+ } catch {
193
+ // Missing home dir / unreadable models.json must not block the extension.
194
+ }
195
+ }
196
+
179
197
  const fetchUsageSnapshot = async (ctx: ExtensionContext): Promise<UsageSnapshot> => {
180
198
  const credential = await resolveGrokCredential(ctx);
181
199
  if (!credential) {
@@ -732,22 +750,47 @@ export default function betterGrok(pi: ExtensionAPI): void {
732
750
  setStatusWidget(ctx, statusWidgetParts(fast, usage));
733
751
  }
734
752
 
735
- const ensureGrok47 = (ctx: ExtensionContext): void => {
736
- if (typeof pi.registerProvider !== "function") return;
737
- const getAll = ctx.modelRegistry?.getAll;
738
- if (typeof getAll !== "function") return;
753
+ const ensureGrok47 = async (ctx: ExtensionContext): Promise<void> => {
754
+ if (!process.env.VITEST) {
755
+ try {
756
+ ensureGrok47InModelsJsonFile(piAgentDir());
757
+ } catch {
758
+ // Keep going; in-memory fallback below may still register the model.
759
+ }
760
+ }
739
761
  try {
740
- registerGrok47OnProviders(
741
- (name, config) => pi.registerProvider(name, config),
742
- getAll.call(ctx.modelRegistry),
743
- );
762
+ await ctx.modelRegistry?.refresh?.({ allowNetwork: false });
744
763
  } catch {
745
- // Catalog registration must not break session startup.
764
+ // Offline refresh failures should not block session startup.
765
+ }
766
+ const registry = ctx.modelRegistry;
767
+ if (typeof registry?.find === "function" && !registry.find("xai", GROK_47_ID)) {
768
+ const getAll = registry.getAll;
769
+ if (typeof pi.registerProvider === "function" && typeof getAll === "function") {
770
+ try {
771
+ registerGrok47OnProviders(
772
+ (name, config) => pi.registerProvider(name, config),
773
+ getAll.call(registry),
774
+ );
775
+ } catch {
776
+ // Catalog registration must not break session startup.
777
+ }
778
+ }
746
779
  }
780
+ const branch =
781
+ typeof ctx.sessionManager.getBranch === "function"
782
+ ? ctx.sessionManager.getBranch()
783
+ : typeof ctx.sessionManager.getEntries === "function"
784
+ ? ctx.sessionManager.getEntries()
785
+ : [];
786
+ const saved = lastSessionModel(branch);
787
+ if (!shouldRestoreGrok47(ctx.model, saved) || !saved) return;
788
+ const restored = registry?.find?.(saved.provider, saved.modelId);
789
+ if (restored) await pi.setModel(restored);
747
790
  };
748
791
 
749
792
  pi.on("session_start", (_event, ctx) => {
750
- ensureGrok47(ctx);
793
+ void ensureGrok47(ctx);
751
794
  invalidateContextUsage();
752
795
  invalidateSessionName();
753
796
  multiproviderRefreshCtx = ctx;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-better-grok",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Improve Grok/xAI in pi with fast mode, subscription usage stats, banked reset redemption, multiprovider pools, footer polish, and settings — mirroring pi-better-openai.",
5
5
  "keywords": [
6
6
  "footer",
package/src/xai-models.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
1
3
  import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
2
4
 
3
5
  export const GROK_47_ID = "grok-4.7";
@@ -14,10 +16,27 @@ const GROK_47_LONG_CONTEXT_TIER = {
14
16
  cacheWrite: 0,
15
17
  } as const;
16
18
 
19
+ const GROK_47_COST = {
20
+ input: 2,
21
+ output: 6,
22
+ cacheRead: 0.5,
23
+ cacheWrite: 0,
24
+ tiers: [{ ...GROK_47_LONG_CONTEXT_TIER }],
25
+ };
26
+
27
+ const GROK_47_THINKING_LEVEL_MAP = {
28
+ off: null,
29
+ minimal: null,
30
+ low: "low",
31
+ medium: "medium",
32
+ high: "high",
33
+ xhigh: "xhigh",
34
+ max: null,
35
+ } as const;
36
+
17
37
  /**
18
38
  * Native xAI Grok 4.7, cloned from pi-core's grok-4.6 catalog plus the public
19
- * 4.7 card: 500k context, $2/$6 per 1M, cache read $0.50, 2x rates above 200k,
20
- * reasoning efforts low/medium/high/xhigh. Used only when a sibling template exists.
39
+ * 4.7 card. Used as an in-memory fallback when models.json cannot be updated.
21
40
  */
22
41
  export const GROK_47_FALLBACK: ProviderModelConfig = {
23
42
  id: GROK_47_ID,
@@ -25,23 +44,25 @@ export const GROK_47_FALLBACK: ProviderModelConfig = {
25
44
  api: "openai-responses",
26
45
  baseUrl: "https://api.x.ai/v1",
27
46
  reasoning: true,
28
- thinkingLevelMap: {
29
- off: null,
30
- minimal: null,
31
- low: "low",
32
- medium: "medium",
33
- high: "high",
34
- xhigh: "xhigh",
35
- max: null,
36
- },
47
+ thinkingLevelMap: { ...GROK_47_THINKING_LEVEL_MAP },
37
48
  input: ["text", "image"],
38
- cost: {
39
- input: 2,
40
- output: 6,
41
- cacheRead: 0.5,
42
- cacheWrite: 0,
43
- tiers: [{ ...GROK_47_LONG_CONTEXT_TIER }],
44
- },
49
+ cost: { ...GROK_47_COST, tiers: [{ ...GROK_47_LONG_CONTEXT_TIER }] },
50
+ contextWindow: 500_000,
51
+ maxTokens: 500_000,
52
+ compat: { supportsLongCacheRetention: false },
53
+ };
54
+
55
+ /**
56
+ * models.json custom model layered onto builtin `xai`. api/baseUrl are omitted so
57
+ * pi inherits them from grok-4.6 instead of replacing the provider catalog.
58
+ */
59
+ export const GROK_47_MODELS_JSON_DEFINITION = {
60
+ id: GROK_47_ID,
61
+ name: GROK_47_NAME,
62
+ reasoning: true,
63
+ thinkingLevelMap: { ...GROK_47_THINKING_LEVEL_MAP },
64
+ input: ["text", "image"] as Array<"text" | "image">,
65
+ cost: { ...GROK_47_COST, tiers: [{ ...GROK_47_LONG_CONTEXT_TIER }] },
45
66
  contextWindow: 500_000,
46
67
  maxTokens: 500_000,
47
68
  compat: { supportsLongCacheRetention: false },
@@ -160,3 +181,81 @@ export function registerGrok47OnProviders(
160
181
  }
161
182
  return registered;
162
183
  }
184
+
185
+ function asObject(value: unknown): Record<string, unknown> | undefined {
186
+ return value && typeof value === "object" && !Array.isArray(value)
187
+ ? (value as Record<string, unknown>)
188
+ : undefined;
189
+ }
190
+
191
+ export function upsertGrok47InModelsJson(raw: unknown): {
192
+ next: Record<string, unknown>;
193
+ changed: boolean;
194
+ } {
195
+ const root = { ...asObject(raw) };
196
+ const providers = { ...asObject(root.providers) };
197
+ const xai = { ...asObject(providers.xai) };
198
+ const models = Array.isArray(xai.models) ? [...xai.models] : [];
199
+ const alreadyPresent = models.some((entry) => asObject(entry)?.id === GROK_47_ID);
200
+ if (alreadyPresent) {
201
+ return { next: asObject(raw) ?? root, changed: false };
202
+ }
203
+ models.push({
204
+ ...GROK_47_MODELS_JSON_DEFINITION,
205
+ thinkingLevelMap: { ...GROK_47_THINKING_LEVEL_MAP },
206
+ input: [...GROK_47_MODELS_JSON_DEFINITION.input],
207
+ cost: { ...GROK_47_COST, tiers: [{ ...GROK_47_LONG_CONTEXT_TIER }] },
208
+ compat: { ...GROK_47_MODELS_JSON_DEFINITION.compat },
209
+ });
210
+ xai.models = models;
211
+ providers.xai = xai;
212
+ root.providers = providers;
213
+ return { next: root, changed: true };
214
+ }
215
+
216
+ export function grok47ModelsJsonPath(agentDir: string): string {
217
+ return join(agentDir, "models.json");
218
+ }
219
+
220
+ export function ensureGrok47InModelsJsonFile(agentDir: string): boolean {
221
+ const path = grok47ModelsJsonPath(agentDir);
222
+ let parsed: unknown = {};
223
+ if (existsSync(path)) {
224
+ try {
225
+ parsed = JSON.parse(readFileSync(path, "utf8"));
226
+ } catch {
227
+ return false;
228
+ }
229
+ }
230
+ const { next, changed } = upsertGrok47InModelsJson(parsed);
231
+ if (!changed) return false;
232
+ mkdirSync(dirname(path), { recursive: true });
233
+ writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`);
234
+ return true;
235
+ }
236
+
237
+ export type SessionModelRef = { provider: string; modelId: string };
238
+
239
+ export function lastSessionModel(
240
+ entries: ReadonlyArray<{ type: string; provider?: string; modelId?: string }>,
241
+ ): SessionModelRef | undefined {
242
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
243
+ const entry = entries[index];
244
+ if (
245
+ entry?.type === "model_change" &&
246
+ typeof entry.provider === "string" &&
247
+ typeof entry.modelId === "string"
248
+ ) {
249
+ return { provider: entry.provider, modelId: entry.modelId };
250
+ }
251
+ }
252
+ return undefined;
253
+ }
254
+
255
+ export function shouldRestoreGrok47(
256
+ current: { provider?: string; id?: string } | undefined,
257
+ saved: SessionModelRef | undefined,
258
+ ): boolean {
259
+ if (!saved || saved.modelId !== GROK_47_ID) return false;
260
+ return current?.id !== saved.modelId || current?.provider !== saved.provider;
261
+ }