Skip to content

Commit 6a85cdb

Browse files
feat(models): add grok-4.7 model support (#1152)
* feat(models): add grok-4.7 model support Exposes grok-4.7 wherever Grok models are offered, with the 500k context heuristic and the extended reasoning ladder (low/medium/ high default/xhigh) shared with grok-4.6. Prior Grok behavior unchanged. Fixes CL-8786 * refactor(provider): rename grok 4.6 identifiers to include 4.7 * fix(provider): skip grok-4.7 insert when vendor already lists it * feat(zen): map grok-4.7 to the responses protocol * test(provider): cover grok-4.7 skip when first or last
1 parent fc5e047 commit 6a85cdb

11 files changed

Lines changed: 161 additions & 17 deletions

‎packages/zen/src/index.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,9 @@ export {
66
} from "./constants.js";
77
export { resolveZenEndpoint } from "./endpoint.js";
88
export { isZenProvider, isZenProviderId } from "./identity.js";
9-
export { isKnownZenModel, ZEN_DEFAULT_MODEL, ZEN_MODEL_IDS } from "./models.js";
9+
export {
10+
isKnownZenModel,
11+
protocolForZenModel,
12+
ZEN_DEFAULT_MODEL,
13+
ZEN_MODEL_IDS,
14+
} from "./models.js";

‎packages/zen/src/models.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, expect, test } from "bun:test";
2+
3+
import { resolveZenEndpoint } from "./endpoint.js";
4+
import { isKnownZenModel, protocolForZenModel } from "./models.js";
5+
6+
describe("protocolForZenModel", () => {
7+
test("maps grok-4.7 to responses like grok-4.6", () => {
8+
expect(protocolForZenModel("grok-4.7")).toBe("responses");
9+
expect(protocolForZenModel("grok-4.6")).toBe("responses");
10+
expect(isKnownZenModel("grok-4.7")).toBe(true);
11+
expect(isKnownZenModel("grok-4.6")).toBe(true);
12+
});
13+
});
14+
15+
describe("resolveZenEndpoint", () => {
16+
test("routes grok-4.7 the same as grok-4.6", () => {
17+
expect(resolveZenEndpoint("grok-4.7")).toEqual(
18+
resolveZenEndpoint("grok-4.6"),
19+
);
20+
expect(resolveZenEndpoint("grok-4.7").adapter).toBe("openai-responses");
21+
});
22+
});

‎packages/zen/src/models.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const ZEN_MODELS: readonly ZenModel[] = [
6363
{ id: "gpt-5", protocol: "responses" },
6464
{ id: "gpt-5-codex", protocol: "responses" },
6565
{ id: "gpt-5-nano", protocol: "responses" },
66+
{ id: "grok-4.7", protocol: "responses" },
6667
{ id: "grok-4.6", protocol: "responses" },
6768
{ id: "grok-4.5", protocol: "responses" },
6869
{ id: "grok-build-0.1", protocol: "responses" },

‎src/auth/xai/constants.ts‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,38 @@
11
import { XAI_REDIRECT_URI } from "@corbits/xai-provider";
22

3-
// Local alias over the vendor fallback list (CL-5691): XAI_DEFAULT_MODELS
4-
// backs live xAI catalog calls and stays in the vendor package — this alias
5-
// only renames the proxy URL. identity-divergence.test.ts pins the agreement.
6-
export {
7-
XAI_DEFAULT_MODELS,
3+
import {
4+
XAI_DEFAULT_MODELS as VENDOR_XAI_DEFAULT_MODELS,
85
XAI_OAUTH_PROXY_BASE_URL as XAI_BASE_URL,
9-
XAI_REDIRECT_URI,
106
XAI_REFRESH_SKEW_MS,
117
} from "@corbits/xai-provider";
128

9+
export { XAI_BASE_URL, XAI_REDIRECT_URI, XAI_REFRESH_SKEW_MS };
10+
11+
const GROK_47 = "grok-4.7";
12+
13+
// Local extension over the vendor fallback list (CL-5691): grok-4.7 rides the
14+
// same OAuth proxy as the older Grok generations but the vendored catalog has
15+
// not caught up yet. Insert after the second vendor entry and skip when the
16+
// vendor list already includes it so a vendor bump cannot duplicate. The
17+
// default stays the first vendor entry.
18+
export function extendVendorXaiDefaultModels(
19+
vendorModels: readonly string[],
20+
): readonly [string, ...string[]] {
21+
if (vendorModels.some((id) => id === GROK_47)) {
22+
const [first, ...rest] = vendorModels;
23+
if (first === undefined) return [GROK_47];
24+
return [first, ...rest];
25+
}
26+
const [first, second, ...rest] = vendorModels;
27+
if (first === undefined) return [GROK_47];
28+
if (second === undefined) return [first, GROK_47];
29+
return [first, second, GROK_47, ...rest];
30+
}
31+
32+
export const XAI_DEFAULT_MODELS = extendVendorXaiDefaultModels(
33+
VENDOR_XAI_DEFAULT_MODELS,
34+
);
35+
1336
const xaiRedirect = new URL(XAI_REDIRECT_URI);
1437
export const XAI_CALLBACK_PORT = Number(xaiRedirect.port);
1538
export const XAI_CALLBACK_PATH = xaiRedirect.pathname;

‎src/config/xai-providers.test.ts‎

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { describe, expect, test } from "bun:test";
22

3-
import { XAI_BASE_URL, XAI_DEFAULT_MODELS } from "../auth/xai/constants.js";
3+
import {
4+
extendVendorXaiDefaultModels,
5+
XAI_BASE_URL,
6+
XAI_DEFAULT_MODELS,
7+
} from "../auth/xai/constants.js";
48
import type { XaiProfile } from "../auth/xai/store.js";
59
import { providerCatalogToSettings } from "./index.js";
610
import {
@@ -12,12 +16,64 @@ import {
1216
} from "./xai-providers.js";
1317

1418
describe("xAI OAuth provider projection", () => {
15-
test("default models include Grok 4.6 while defaulting to the CLI coding model", () => {
19+
test("default models include Grok 4.7 while defaulting to the CLI coding model", () => {
1620
const models: string[] = [...XAI_DEFAULT_MODELS];
17-
expect(models).toEqual(["grok-4.5", "grok-4.6", "grok-composer-2.5-fast"]);
21+
expect(models).toEqual([
22+
"grok-4.5",
23+
"grok-4.6",
24+
"grok-4.7",
25+
"grok-composer-2.5-fast",
26+
]);
27+
expect(models.filter((m) => m === "grok-4.7")).toHaveLength(1);
1828
expect(models[0]).toBe("grok-4.5");
1929
});
2030

31+
test("skips grok-4.7 insert when the vendor list already includes it", () => {
32+
const vendor = [
33+
"grok-4.5",
34+
"grok-4.6",
35+
"grok-4.7",
36+
"grok-composer-2.5-fast",
37+
] as const;
38+
const models = extendVendorXaiDefaultModels(vendor);
39+
expect(models).toEqual(vendor);
40+
expect(models.filter((m) => m === "grok-4.7")).toHaveLength(1);
41+
});
42+
43+
test("skips grok-4.7 insert when the vendor list starts with it", () => {
44+
const vendor = [
45+
"grok-4.7",
46+
"grok-4.5",
47+
"grok-4.6",
48+
"grok-composer-2.5-fast",
49+
] as const;
50+
const models = extendVendorXaiDefaultModels(vendor);
51+
expect(models).toEqual(vendor);
52+
expect(models.filter((m) => m === "grok-4.7")).toHaveLength(1);
53+
});
54+
55+
test("skips grok-4.7 insert when the vendor list ends with it", () => {
56+
const vendor = [
57+
"grok-4.5",
58+
"grok-4.6",
59+
"grok-composer-2.5-fast",
60+
"grok-4.7",
61+
] as const;
62+
const models = extendVendorXaiDefaultModels(vendor);
63+
expect(models).toEqual(vendor);
64+
expect(models.filter((m) => m === "grok-4.7")).toHaveLength(1);
65+
});
66+
67+
test("inserts grok-4.7 after the second vendor model when absent", () => {
68+
expect(
69+
extendVendorXaiDefaultModels([
70+
"grok-4.5",
71+
"grok-4.6",
72+
"grok-composer-2.5-fast",
73+
]),
74+
).toEqual(["grok-4.5", "grok-4.6", "grok-4.7", "grok-composer-2.5-fast"]);
75+
});
76+
2177
const profile: XaiProfile = {
2278
name: "work",
2379
tokens: {

‎src/provider/context-window.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ describe("contextWindowFor", () => {
2727
expect(contextWindowFor("xai/thegreataxios:grok-4.5")).toBe(500_000);
2828
});
2929

30-
it("falls back to a 500k grok-4.5/4.6 heuristic window when the registry has no entry", () => {
30+
it("falls back to a 500k grok-4.5/4.6/4.7 heuristic window when the registry has no entry", () => {
3131
setModelContextWindows(undefined);
3232
expect(contextWindowFor("xai/thegreataxios:grok-4.5")).toBe(500_000);
3333
expect(contextWindowFor("xai/thegreataxios:grok-4.6")).toBe(500_000);
34+
expect(contextWindowFor("xai/thegreataxios:grok-4.7")).toBe(500_000);
3435
expect(contextWindowFor("xai/thegreataxios:grok-4.3")).toBe(1_000_000);
3536
});
3637

‎src/provider/context-window.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,12 @@ function heuristicWindow(model: string): number {
9393
if (m.includes("glm-5.3")) return 1_000_000;
9494
if (m.includes("glm")) return 200_000;
9595
if (m.includes("o3") || m.includes("o4")) return 200_000;
96-
if (m.includes("grok-4.6") || m.includes("grok-4.5")) return 500_000;
96+
if (
97+
m.includes("grok-4.7") ||
98+
m.includes("grok-4.6") ||
99+
m.includes("grok-4.5")
100+
)
101+
return 500_000;
97102
if (m.includes("grok-4.3")) return 1_000_000;
98103
if (m.includes("grok") || m.includes("xai")) return 256_000;
99104
return DEFAULT_CONTEXT_WINDOW;

‎src/provider/identity-divergence.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ZEN_DEFAULT_MODEL,
1515
ZEN_MODEL_IDS,
1616
isKnownZenModel,
17+
protocolForZenModel,
1718
} from "../../packages/zen/src/index.js";
1819
import {
1920
CODEX_BASE_URL,
@@ -131,6 +132,12 @@ describe("provider identity divergence", () => {
131132
}
132133
});
133134

135+
test("Zen protocol map pins grok-4.7 to responses like grok-4.6", () => {
136+
expect(protocolForZenModel("grok-4.7")).toBe("responses");
137+
expect(protocolForZenModel("grok-4.6")).toBe("responses");
138+
expect(isKnownZenModel("grok-4.7")).toBe(true);
139+
});
140+
134141
test("OpenCode Go registry entry tracks the packaged seed catalog", () => {
135142
const go = firstClassProviderById("opencode-go");
136143
expect([...(go?.models ?? [])]).toEqual([...OPENCODE_GO_MODEL_IDS]);

‎src/provider/reasoning-effort.test.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,15 @@ describe("supportedEfforts", () => {
147147
]);
148148
});
149149

150+
test("grok-4.7 includes xhigh", () => {
151+
expect(supportedEfforts("grok-4.7")).toEqual([
152+
"low",
153+
"medium",
154+
"high",
155+
"xhigh",
156+
]);
157+
});
158+
150159
test("grok-4.5 stays on the unknown-model subset without xhigh", () => {
151160
expect(supportedEfforts("grok-4.5")).toEqual(["low", "medium", "high"]);
152161
});
@@ -223,6 +232,11 @@ describe("validateEffort", () => {
223232
expect(validateEffort("grok-4.5", "xhigh").ok).toBe(false);
224233
});
225234

235+
test("accepts xhigh on grok-4.7", () => {
236+
expect(validateEffort("grok-4.7", "xhigh")).toEqual({ ok: true });
237+
expect(validateEffort("grok-4.7", "minimal").ok).toBe(false);
238+
});
239+
226240
test("accepts minimal on Muse Spark and rejects none", () => {
227241
expect(validateEffort("muse-spark-1.3-contributor", "minimal")).toEqual({
228242
ok: true,
@@ -295,6 +309,12 @@ describe("cycleReasoningEffort", () => {
295309
expect(cycleReasoningEffort("grok-4.6", "high")).toBe("xhigh");
296310
expect(cycleReasoningEffort("grok-4.6", "xhigh")).toBe("low");
297311
});
312+
313+
test("wraps high to xhigh to low on grok-4.7", () => {
314+
expect(cycleReasoningEffort("grok-4.7", undefined)).toBe("xhigh");
315+
expect(cycleReasoningEffort("grok-4.7", "high")).toBe("xhigh");
316+
expect(cycleReasoningEffort("grok-4.7", "xhigh")).toBe("low");
317+
});
298318
});
299319

300320
describe("reasoning capability gate", () => {
@@ -504,6 +524,7 @@ describe("defaultEffortForModel", () => {
504524
afterEach(() => setModelReasoningCapabilities({}));
505525

506526
test("grok family defaults to high", () => {
527+
expect(defaultEffortForModel("grok-4.7")).toBe("high");
507528
expect(defaultEffortForModel("grok-4.6")).toBe("high");
508529
expect(defaultEffortForModel("grok-4.5")).toBe("high");
509530
});

‎src/provider/reasoning-effort.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ function isMuseSparkModel(model: string): boolean {
8181
return /^muse-spark/i.test(model.trim());
8282
}
8383

84-
// grok-4.6 accepts xhigh — the same ladder as CODEX_EFFORTS above;
84+
// grok-4.6 and grok-4.7 accept xhigh — the same ladder as CODEX_EFFORTS above;
8585
// grok-4.5 and composer stay on the unknown-model subset.
86-
const GROK_46_EFFORTS: readonly ReasoningEffort[] = CODEX_EFFORTS;
87-
const GROK_46_MODELS: readonly string[] = ["grok-4.6"];
86+
const GROK_46_AND_47_EFFORTS: readonly ReasoningEffort[] = CODEX_EFFORTS;
87+
const GROK_46_AND_47_MODELS: readonly string[] = ["grok-4.6", "grok-4.7"];
8888

8989
// GPT-6 Astra accepts low through max on both the OpenAI API and Codex surfaces.
9090
// Not ultra (gpt-5.6 Codex-only), not minimal, not none.
@@ -153,8 +153,8 @@ export function supportedEfforts(
153153
if (isKnownOpenAIReasoningModel(model)) {
154154
return [...DEFAULT_EFFORTS];
155155
}
156-
if (GROK_46_MODELS.includes(model)) {
157-
return [...GROK_46_EFFORTS];
156+
if (GROK_46_AND_47_MODELS.includes(model)) {
157+
return [...GROK_46_AND_47_EFFORTS];
158158
}
159159
if (GLM_53_MODELS.includes(model)) {
160160
return [...GLM_53_EFFORTS];

0 commit comments

Comments
 (0)