From b771e27405e9ff19b835479643644fe293ad11f5 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Mon, 21 Sep 2026 11:48:19 -0700 Subject: [PATCH 1/5] 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 --- src/auth/xai/constants.ts | 23 +++++++++++++++++------ src/config/xai-providers.test.ts | 9 +++++++-- src/provider/context-window.test.ts | 3 ++- src/provider/context-window.ts | 7 ++++++- src/provider/reasoning-effort.test.ts | 21 +++++++++++++++++++++ src/provider/reasoning-effort.ts | 4 ++-- src/subagent/provider-family.test.ts | 3 +++ 7 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/auth/xai/constants.ts b/src/auth/xai/constants.ts index 48de2fb4f..0bfc9e72f 100644 --- a/src/auth/xai/constants.ts +++ b/src/auth/xai/constants.ts @@ -1,15 +1,26 @@ import { XAI_REDIRECT_URI } from "@corbits/xai-provider"; -// Local alias over the vendor fallback list (CL-5691): XAI_DEFAULT_MODELS -// backs live xAI catalog calls and stays in the vendor package — this alias -// only renames the proxy URL. identity-divergence.test.ts pins the agreement. -export { - XAI_DEFAULT_MODELS, +import { + XAI_DEFAULT_MODELS as VENDOR_XAI_DEFAULT_MODELS, XAI_OAUTH_PROXY_BASE_URL as XAI_BASE_URL, - XAI_REDIRECT_URI, XAI_REFRESH_SKEW_MS, } from "@corbits/xai-provider"; +export { XAI_BASE_URL, XAI_REDIRECT_URI, XAI_REFRESH_SKEW_MS }; + +// Local extension over the vendor fallback list (CL-5691): grok-4.7 rides the +// same OAuth proxy as the older Grok generations but the vendored catalog has +// not caught up yet. The default stays the first vendor entry; drop this shim +// once the vendor fallback lists grok-4.7 itself. +const [firstVendorModel, secondVendorModel, ...restVendorModels] = + VENDOR_XAI_DEFAULT_MODELS; +export const XAI_DEFAULT_MODELS = [ + firstVendorModel, + secondVendorModel, + "grok-4.7", + ...restVendorModels, +] as const; + const xaiRedirect = new URL(XAI_REDIRECT_URI); export const XAI_CALLBACK_PORT = Number(xaiRedirect.port); export const XAI_CALLBACK_PATH = xaiRedirect.pathname; diff --git a/src/config/xai-providers.test.ts b/src/config/xai-providers.test.ts index 5b6d036a0..c50a01eb1 100644 --- a/src/config/xai-providers.test.ts +++ b/src/config/xai-providers.test.ts @@ -12,9 +12,14 @@ import { } from "./xai-providers.js"; describe("xAI OAuth provider projection", () => { - test("default models include Grok 4.6 while defaulting to the CLI coding model", () => { + test("default models include Grok 4.7 while defaulting to the CLI coding model", () => { const models: string[] = [...XAI_DEFAULT_MODELS]; - expect(models).toEqual(["grok-4.5", "grok-4.6", "grok-composer-2.5-fast"]); + expect(models).toEqual([ + "grok-4.5", + "grok-4.6", + "grok-4.7", + "grok-composer-2.5-fast", + ]); expect(models[0]).toBe("grok-4.5"); }); diff --git a/src/provider/context-window.test.ts b/src/provider/context-window.test.ts index 088f72a49..350471287 100644 --- a/src/provider/context-window.test.ts +++ b/src/provider/context-window.test.ts @@ -27,10 +27,11 @@ describe("contextWindowFor", () => { expect(contextWindowFor("xai/thegreataxios:grok-4.5")).toBe(500_000); }); - it("falls back to a 500k grok-4.5/4.6 heuristic window when the registry has no entry", () => { + it("falls back to a 500k grok-4.5/4.6/4.7 heuristic window when the registry has no entry", () => { setModelContextWindows(undefined); expect(contextWindowFor("xai/thegreataxios:grok-4.5")).toBe(500_000); expect(contextWindowFor("xai/thegreataxios:grok-4.6")).toBe(500_000); + expect(contextWindowFor("xai/thegreataxios:grok-4.7")).toBe(500_000); expect(contextWindowFor("xai/thegreataxios:grok-4.3")).toBe(1_000_000); }); diff --git a/src/provider/context-window.ts b/src/provider/context-window.ts index 7a07fe98d..f5ab76be5 100644 --- a/src/provider/context-window.ts +++ b/src/provider/context-window.ts @@ -93,7 +93,12 @@ function heuristicWindow(model: string): number { if (m.includes("glm-5.3")) return 1_000_000; if (m.includes("glm")) return 200_000; if (m.includes("o3") || m.includes("o4")) return 200_000; - if (m.includes("grok-4.6") || m.includes("grok-4.5")) return 500_000; + if ( + m.includes("grok-4.7") || + m.includes("grok-4.6") || + m.includes("grok-4.5") + ) + return 500_000; if (m.includes("grok-4.3")) return 1_000_000; if (m.includes("grok") || m.includes("xai")) return 256_000; return DEFAULT_CONTEXT_WINDOW; diff --git a/src/provider/reasoning-effort.test.ts b/src/provider/reasoning-effort.test.ts index d658b227d..a1d7d4f66 100644 --- a/src/provider/reasoning-effort.test.ts +++ b/src/provider/reasoning-effort.test.ts @@ -147,6 +147,15 @@ describe("supportedEfforts", () => { ]); }); + test("grok-4.7 includes xhigh", () => { + expect(supportedEfforts("grok-4.7")).toEqual([ + "low", + "medium", + "high", + "xhigh", + ]); + }); + test("grok-4.5 stays on the unknown-model subset without xhigh", () => { expect(supportedEfforts("grok-4.5")).toEqual(["low", "medium", "high"]); }); @@ -223,6 +232,11 @@ describe("validateEffort", () => { expect(validateEffort("grok-4.5", "xhigh").ok).toBe(false); }); + test("accepts xhigh on grok-4.7", () => { + expect(validateEffort("grok-4.7", "xhigh")).toEqual({ ok: true }); + expect(validateEffort("grok-4.7", "minimal").ok).toBe(false); + }); + test("accepts minimal on Muse Spark and rejects none", () => { expect(validateEffort("muse-spark-1.3-contributor", "minimal")).toEqual({ ok: true, @@ -295,6 +309,12 @@ describe("cycleReasoningEffort", () => { expect(cycleReasoningEffort("grok-4.6", "high")).toBe("xhigh"); expect(cycleReasoningEffort("grok-4.6", "xhigh")).toBe("low"); }); + + test("wraps high to xhigh to low on grok-4.7", () => { + expect(cycleReasoningEffort("grok-4.7", undefined)).toBe("xhigh"); + expect(cycleReasoningEffort("grok-4.7", "high")).toBe("xhigh"); + expect(cycleReasoningEffort("grok-4.7", "xhigh")).toBe("low"); + }); }); describe("reasoning capability gate", () => { @@ -504,6 +524,7 @@ describe("defaultEffortForModel", () => { afterEach(() => setModelReasoningCapabilities({})); test("grok family defaults to high", () => { + expect(defaultEffortForModel("grok-4.7")).toBe("high"); expect(defaultEffortForModel("grok-4.6")).toBe("high"); expect(defaultEffortForModel("grok-4.5")).toBe("high"); }); diff --git a/src/provider/reasoning-effort.ts b/src/provider/reasoning-effort.ts index b436df70d..68324635b 100644 --- a/src/provider/reasoning-effort.ts +++ b/src/provider/reasoning-effort.ts @@ -81,10 +81,10 @@ function isMuseSparkModel(model: string): boolean { return /^muse-spark/i.test(model.trim()); } -// grok-4.6 accepts xhigh — the same ladder as CODEX_EFFORTS above; +// grok-4.6 and grok-4.7 accept xhigh — the same ladder as CODEX_EFFORTS above; // grok-4.5 and composer stay on the unknown-model subset. const GROK_46_EFFORTS: readonly ReasoningEffort[] = CODEX_EFFORTS; -const GROK_46_MODELS: readonly string[] = ["grok-4.6"]; +const GROK_46_MODELS: readonly string[] = ["grok-4.6", "grok-4.7"]; // GPT-6 Astra accepts low through max on both the OpenAI API and Codex surfaces. // Not ultra (gpt-5.6 Codex-only), not minimal, not none. diff --git a/src/subagent/provider-family.test.ts b/src/subagent/provider-family.test.ts index 4b6f76d48..4970d8f34 100644 --- a/src/subagent/provider-family.test.ts +++ b/src/subagent/provider-family.test.ts @@ -131,6 +131,9 @@ describe("detectModelFamily", () => { expect( detectModelFamily({ providerName: "xai/default", model: "grok-4.6" }), ).toBe("grok"); + expect( + detectModelFamily({ providerName: "xai/default", model: "grok-4.7" }), + ).toBe("grok"); expect( detectModelFamily({ providerName: "moonshot", model: "kimi-k2" }), ).toBe("kimi"); From f8f7e4dcdc69efe0c1db358d3273b75a2cb5dda5 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Mon, 21 Sep 2026 16:30:08 -0700 Subject: [PATCH 2/5] refactor(provider): rename grok 4.6 identifiers to include 4.7 --- src/config/xai-providers.test.ts | 1 + src/provider/reasoning-effort.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/config/xai-providers.test.ts b/src/config/xai-providers.test.ts index c50a01eb1..3af697a8c 100644 --- a/src/config/xai-providers.test.ts +++ b/src/config/xai-providers.test.ts @@ -20,6 +20,7 @@ describe("xAI OAuth provider projection", () => { "grok-4.7", "grok-composer-2.5-fast", ]); + expect(models.filter((m) => m === "grok-4.7")).toHaveLength(1); expect(models[0]).toBe("grok-4.5"); }); diff --git a/src/provider/reasoning-effort.ts b/src/provider/reasoning-effort.ts index 68324635b..8254c6e89 100644 --- a/src/provider/reasoning-effort.ts +++ b/src/provider/reasoning-effort.ts @@ -83,8 +83,8 @@ function isMuseSparkModel(model: string): boolean { // grok-4.6 and grok-4.7 accept xhigh — the same ladder as CODEX_EFFORTS above; // grok-4.5 and composer stay on the unknown-model subset. -const GROK_46_EFFORTS: readonly ReasoningEffort[] = CODEX_EFFORTS; -const GROK_46_MODELS: readonly string[] = ["grok-4.6", "grok-4.7"]; +const GROK_46_AND_47_EFFORTS: readonly ReasoningEffort[] = CODEX_EFFORTS; +const GROK_46_AND_47_MODELS: readonly string[] = ["grok-4.6", "grok-4.7"]; // GPT-6 Astra accepts low through max on both the OpenAI API and Codex surfaces. // Not ultra (gpt-5.6 Codex-only), not minimal, not none. @@ -153,8 +153,8 @@ export function supportedEfforts( if (isKnownOpenAIReasoningModel(model)) { return [...DEFAULT_EFFORTS]; } - if (GROK_46_MODELS.includes(model)) { - return [...GROK_46_EFFORTS]; + if (GROK_46_AND_47_MODELS.includes(model)) { + return [...GROK_46_AND_47_EFFORTS]; } if (GLM_53_MODELS.includes(model)) { return [...GLM_53_EFFORTS]; From 57a376ff4fb6e399fc5476704c72eed84c4ea783 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Mon, 21 Sep 2026 16:53:27 -0700 Subject: [PATCH 3/5] fix(provider): skip grok-4.7 insert when vendor already lists it --- src/auth/xai/constants.ts | 32 ++++++++++++++++++++++---------- src/config/xai-providers.test.ts | 28 +++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/auth/xai/constants.ts b/src/auth/xai/constants.ts index 0bfc9e72f..feedc1da6 100644 --- a/src/auth/xai/constants.ts +++ b/src/auth/xai/constants.ts @@ -8,18 +8,30 @@ import { export { XAI_BASE_URL, XAI_REDIRECT_URI, XAI_REFRESH_SKEW_MS }; +const GROK_47 = "grok-4.7"; + // Local extension over the vendor fallback list (CL-5691): grok-4.7 rides the // same OAuth proxy as the older Grok generations but the vendored catalog has -// not caught up yet. The default stays the first vendor entry; drop this shim -// once the vendor fallback lists grok-4.7 itself. -const [firstVendorModel, secondVendorModel, ...restVendorModels] = - VENDOR_XAI_DEFAULT_MODELS; -export const XAI_DEFAULT_MODELS = [ - firstVendorModel, - secondVendorModel, - "grok-4.7", - ...restVendorModels, -] as const; +// not caught up yet. Insert after the second vendor entry and skip when the +// vendor list already includes it so a vendor bump cannot duplicate. The +// default stays the first vendor entry. +export function extendVendorXaiDefaultModels( + vendorModels: readonly string[], +): readonly [string, ...string[]] { + if (vendorModels.some((id) => id === GROK_47)) { + const [first, ...rest] = vendorModels; + if (first === undefined) return [GROK_47]; + return [first, ...rest]; + } + const [first, second, ...rest] = vendorModels; + if (first === undefined) return [GROK_47]; + if (second === undefined) return [first, GROK_47]; + return [first, second, GROK_47, ...rest]; +} + +export const XAI_DEFAULT_MODELS = extendVendorXaiDefaultModels( + VENDOR_XAI_DEFAULT_MODELS, +); const xaiRedirect = new URL(XAI_REDIRECT_URI); export const XAI_CALLBACK_PORT = Number(xaiRedirect.port); diff --git a/src/config/xai-providers.test.ts b/src/config/xai-providers.test.ts index 3af697a8c..82ca70a2e 100644 --- a/src/config/xai-providers.test.ts +++ b/src/config/xai-providers.test.ts @@ -1,6 +1,10 @@ import { describe, expect, test } from "bun:test"; -import { XAI_BASE_URL, XAI_DEFAULT_MODELS } from "../auth/xai/constants.js"; +import { + extendVendorXaiDefaultModels, + XAI_BASE_URL, + XAI_DEFAULT_MODELS, +} from "../auth/xai/constants.js"; import type { XaiProfile } from "../auth/xai/store.js"; import { providerCatalogToSettings } from "./index.js"; import { @@ -24,6 +28,28 @@ describe("xAI OAuth provider projection", () => { expect(models[0]).toBe("grok-4.5"); }); + test("skips grok-4.7 insert when the vendor list already includes it", () => { + const vendor = [ + "grok-4.5", + "grok-4.6", + "grok-4.7", + "grok-composer-2.5-fast", + ] as const; + const models = extendVendorXaiDefaultModels(vendor); + expect(models).toEqual(vendor); + expect(models.filter((m) => m === "grok-4.7")).toHaveLength(1); + }); + + test("inserts grok-4.7 after the second vendor model when absent", () => { + expect( + extendVendorXaiDefaultModels([ + "grok-4.5", + "grok-4.6", + "grok-composer-2.5-fast", + ]), + ).toEqual(["grok-4.5", "grok-4.6", "grok-4.7", "grok-composer-2.5-fast"]); + }); + const profile: XaiProfile = { name: "work", tokens: { From 49769ff90e9187c247d62f87cd1853039feee8de Mon Sep 17 00:00:00 2001 From: Sawyer Date: Mon, 21 Sep 2026 16:53:29 -0700 Subject: [PATCH 4/5] feat(zen): map grok-4.7 to the responses protocol --- packages/zen/src/index.ts | 7 ++++++- packages/zen/src/models.test.ts | 22 ++++++++++++++++++++++ packages/zen/src/models.ts | 1 + src/provider/identity-divergence.test.ts | 7 +++++++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 packages/zen/src/models.test.ts diff --git a/packages/zen/src/index.ts b/packages/zen/src/index.ts index f3e18c884..46f5dfec5 100644 --- a/packages/zen/src/index.ts +++ b/packages/zen/src/index.ts @@ -6,4 +6,9 @@ export { } from "./constants.js"; export { resolveZenEndpoint } from "./endpoint.js"; export { isZenProvider, isZenProviderId } from "./identity.js"; -export { isKnownZenModel, ZEN_DEFAULT_MODEL, ZEN_MODEL_IDS } from "./models.js"; +export { + isKnownZenModel, + protocolForZenModel, + ZEN_DEFAULT_MODEL, + ZEN_MODEL_IDS, +} from "./models.js"; diff --git a/packages/zen/src/models.test.ts b/packages/zen/src/models.test.ts new file mode 100644 index 000000000..e90f9e888 --- /dev/null +++ b/packages/zen/src/models.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from "bun:test"; + +import { resolveZenEndpoint } from "./endpoint.js"; +import { isKnownZenModel, protocolForZenModel } from "./models.js"; + +describe("protocolForZenModel", () => { + test("maps grok-4.7 to responses like grok-4.6", () => { + expect(protocolForZenModel("grok-4.7")).toBe("responses"); + expect(protocolForZenModel("grok-4.6")).toBe("responses"); + expect(isKnownZenModel("grok-4.7")).toBe(true); + expect(isKnownZenModel("grok-4.6")).toBe(true); + }); +}); + +describe("resolveZenEndpoint", () => { + test("routes grok-4.7 the same as grok-4.6", () => { + expect(resolveZenEndpoint("grok-4.7")).toEqual( + resolveZenEndpoint("grok-4.6"), + ); + expect(resolveZenEndpoint("grok-4.7").adapter).toBe("openai-responses"); + }); +}); diff --git a/packages/zen/src/models.ts b/packages/zen/src/models.ts index 41205919a..96e26ce4b 100644 --- a/packages/zen/src/models.ts +++ b/packages/zen/src/models.ts @@ -63,6 +63,7 @@ const ZEN_MODELS: readonly ZenModel[] = [ { id: "gpt-5", protocol: "responses" }, { id: "gpt-5-codex", protocol: "responses" }, { id: "gpt-5-nano", protocol: "responses" }, + { id: "grok-4.7", protocol: "responses" }, { id: "grok-4.6", protocol: "responses" }, { id: "grok-4.5", protocol: "responses" }, { id: "grok-build-0.1", protocol: "responses" }, diff --git a/src/provider/identity-divergence.test.ts b/src/provider/identity-divergence.test.ts index 3ee6ef89d..b8c862f68 100644 --- a/src/provider/identity-divergence.test.ts +++ b/src/provider/identity-divergence.test.ts @@ -14,6 +14,7 @@ import { ZEN_DEFAULT_MODEL, ZEN_MODEL_IDS, isKnownZenModel, + protocolForZenModel, } from "../../packages/zen/src/index.js"; import { CODEX_BASE_URL, @@ -131,6 +132,12 @@ describe("provider identity divergence", () => { } }); + test("Zen protocol map pins grok-4.7 to responses like grok-4.6", () => { + expect(protocolForZenModel("grok-4.7")).toBe("responses"); + expect(protocolForZenModel("grok-4.6")).toBe("responses"); + expect(isKnownZenModel("grok-4.7")).toBe(true); + }); + test("OpenCode Go registry entry tracks the packaged seed catalog", () => { const go = firstClassProviderById("opencode-go"); expect([...(go?.models ?? [])]).toEqual([...OPENCODE_GO_MODEL_IDS]); From 544d0fbbc63cc756dc901dc3366b4a28e5585764 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Mon, 21 Sep 2026 17:06:56 -0700 Subject: [PATCH 5/5] test(provider): cover grok-4.7 skip when first or last --- src/config/xai-providers.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/config/xai-providers.test.ts b/src/config/xai-providers.test.ts index 82ca70a2e..372928ac1 100644 --- a/src/config/xai-providers.test.ts +++ b/src/config/xai-providers.test.ts @@ -40,6 +40,30 @@ describe("xAI OAuth provider projection", () => { expect(models.filter((m) => m === "grok-4.7")).toHaveLength(1); }); + test("skips grok-4.7 insert when the vendor list starts with it", () => { + const vendor = [ + "grok-4.7", + "grok-4.5", + "grok-4.6", + "grok-composer-2.5-fast", + ] as const; + const models = extendVendorXaiDefaultModels(vendor); + expect(models).toEqual(vendor); + expect(models.filter((m) => m === "grok-4.7")).toHaveLength(1); + }); + + test("skips grok-4.7 insert when the vendor list ends with it", () => { + const vendor = [ + "grok-4.5", + "grok-4.6", + "grok-composer-2.5-fast", + "grok-4.7", + ] as const; + const models = extendVendorXaiDefaultModels(vendor); + expect(models).toEqual(vendor); + expect(models.filter((m) => m === "grok-4.7")).toHaveLength(1); + }); + test("inserts grok-4.7 after the second vendor model when absent", () => { expect( extendVendorXaiDefaultModels([