FN-7712: fix Grok CLI model list parsing for real grok models output
Fixes the Grok CLI model picker showing raw prompt/preamble text instead of real model names by rewriting parseModelLines to match the actual verified `grok models` output shape. - Rewrote parseModelLines in process-manager.ts to strip the login/"Default model:"/"Available models:" preamble - Strip `*`/`-` bullet markers and the `(default)` annotation from each model line - Preserve existing legacy `id - Label`, columnar, and JSON parsing paths - Added regression tests covering the real grok models output shape - Added changeset (patch) documenting the fix Files changed: .changeset/fn-7712-grok-model-parse.md | 7 ++++ .../src/__tests__/process-manager.test.ts | 39 ++++++++++++++++++++++ .../src/process-manager.ts | 34 ++++++++++++------- 3 files changed, 68 insertions(+), 12 deletions(-) Fusion-Task-Id: FN-7712 Fusion-Task-Lineage: 93e34513-07b9-41b3-8b8b-ecdb763b4208 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7712-grok-model-parse.md
Normal file
7
.changeset/fn-7712-grok-model-parse.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Fix Grok CLI model picker showing prompt text instead of real model names.
|
||||
category: fix
|
||||
dev: Rewrote parseModelLines in fusion-plugin-grok-runtime/process-manager.ts to strip the login/"Default model:"/"Available models:" preamble and `*`/`-` bullet markers plus the `(default)` annotation from verified `grok models` output; legacy `id - Label`, columnar, and JSON paths preserved.
|
||||
@@ -16,6 +16,20 @@ const DASH_MODELS_OUTPUT = [
|
||||
|
||||
const COLUMN_MODELS_OUTPUT = ["grok-4 $5.00/M in", "grok-4-fast $0.20/M in"].join("\n");
|
||||
|
||||
// FN-7712: verified real `grok models` output shape (attachment 1871.png) —
|
||||
// login/session preamble, "Default model:" line, "Available models:" header,
|
||||
// then a bulleted list with `* <id> (default)` for the active model and
|
||||
// `- <id>` for the rest.
|
||||
const REAL_BULLETED_OUTPUT = [
|
||||
"You are logged in with grok-cli v1.2.3",
|
||||
"Default model: grok-4.5",
|
||||
"Available models:",
|
||||
"* grok-4.5 (default)",
|
||||
"- grok-composer-2.5-fast",
|
||||
"- grok-4-fast-reasoning",
|
||||
"- grok-3-mini",
|
||||
].join("\n");
|
||||
|
||||
describe("discoverGrokModels", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -39,6 +53,31 @@ describe("discoverGrokModels", () => {
|
||||
expect(result.fallbackUsed).toBe(false);
|
||||
});
|
||||
|
||||
it("extracts clean model ids from the verified real bulleted output, dropping preamble and markers", async () => {
|
||||
vi.mocked(runGrokCommand).mockResolvedValueOnce({ code: 0, stdout: REAL_BULLETED_OUTPUT, stderr: "" });
|
||||
const result = await discoverGrokModels("grok");
|
||||
|
||||
expect(result.models).toEqual(["grok-4.5", "grok-composer-2.5-fast", "grok-4-fast-reasoning", "grok-3-mini"]);
|
||||
expect(result.source).toBe("models-text");
|
||||
expect(result.fallbackUsed).toBe(false);
|
||||
for (const model of result.models) {
|
||||
expect(model).not.toMatch(/^[*-]/);
|
||||
expect(model).not.toMatch(/\(default\)/i);
|
||||
}
|
||||
expect(result.models.some((m) => /logged in|default model|available models/i.test(m))).toBe(false);
|
||||
});
|
||||
|
||||
it("strips the ` (default)` suffix marker from a bulleted default-model line", async () => {
|
||||
vi.mocked(runGrokCommand).mockResolvedValueOnce({
|
||||
code: 0,
|
||||
stdout: ["Available models:", "* grok-4.5 (default)"].join("\n"),
|
||||
stderr: "",
|
||||
});
|
||||
const result = await discoverGrokModels("grok");
|
||||
|
||||
expect(result.models).toEqual(["grok-4.5"]);
|
||||
});
|
||||
|
||||
it("extracts bare ids from columnar/pricing-separated output", async () => {
|
||||
vi.mocked(runGrokCommand).mockResolvedValueOnce({ code: 0, stdout: COLUMN_MODELS_OUTPUT, stderr: "" });
|
||||
const result = await discoverGrokModels("grok");
|
||||
|
||||
@@ -1,28 +1,38 @@
|
||||
import { runGrokCommand } from "./cli-spawn.js";
|
||||
|
||||
/*
|
||||
FNXC:GrokCli 2026-07-08-00:00:
|
||||
FN-7705: the exact `grok models` output line shape is
|
||||
`upstream-pending-verification` — the upstream README documents the command
|
||||
lists available Grok models "with pricing hints" but does not pin an exact
|
||||
column/separator format. We parse CONSERVATIVELY: strip obvious header/tip/
|
||||
empty-state lines, then take the leading token before a ` - ` label
|
||||
separator (mirroring the Cursor CLI's `<id> - <Label>` shape) or, absent
|
||||
that, before the first run of 2+ spaces (a common columnar pricing-hint
|
||||
layout), falling back to the whole trimmed line as a single-token id.
|
||||
Defensive JSON-tolerant parsing is attempted first (mirroring Cursor's
|
||||
process-manager.ts), even though the real CLI is not known to emit JSON.
|
||||
FNXC:GrokCli 2026-07-08-19:30:
|
||||
FN-7712: the `grok models` output shape is now VERIFIED from a real capture
|
||||
(attachment 1871.png in FN-7712): the CLI prints a login/session preamble
|
||||
("You are logged in with grok..."), a "Default model: <id>" line, an
|
||||
"Available models:" header, then a bulleted list — `* <id> (default)` for
|
||||
the active model and `- <id>` for the rest. We drop the preamble/header
|
||||
lines (case-insensitively, tolerating a trailing colon and leading/trailing
|
||||
whitespace), strip a leading `* `/`- ` bullet marker from each remaining
|
||||
candidate line, then strip a trailing ` (default)`/`(default)` annotation
|
||||
before id extraction so the picker only ever sees bare model ids. Legacy
|
||||
extraction (leading token before a ` - ` label separator, else before the
|
||||
first run of 2+ spaces for columnar/pricing layouts, else the whole trimmed
|
||||
token) is preserved unchanged so older `id - Label (pricing)` and columnar
|
||||
fixtures still parse. Defensive JSON-tolerant parsing is attempted first
|
||||
(mirroring Cursor's process-manager.ts), even though the real CLI is not
|
||||
known to emit JSON.
|
||||
*/
|
||||
function parseModelLines(raw: string): string[] {
|
||||
const ids = raw
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.filter((line) => !/^available models$/i.test(line))
|
||||
.filter((line) => !/^available models:?$/i.test(line))
|
||||
.filter((line) => !/^you are logged in\b/i.test(line))
|
||||
.filter((line) => !/^default model:?/i.test(line))
|
||||
.filter((line) => !/^models?:?$/i.test(line))
|
||||
.filter((line) => !/^no models? available/i.test(line))
|
||||
.filter((line) => !/^tip:/i.test(line))
|
||||
.filter((line) => !/^usage/i.test(line))
|
||||
.map((line) => line.replace(/^[*-]\s+/, ""))
|
||||
.map((line) => line.replace(/\s*\(default\)\s*$/i, ""))
|
||||
.map((line) => line.trim())
|
||||
.map((line) => {
|
||||
const dashIndex = line.indexOf(" - ");
|
||||
if (dashIndex !== -1) return line.slice(0, dashIndex).trim();
|
||||
|
||||
Reference in New Issue
Block a user