From 06d46596d88f0c826a08d415ac7e8a69a55d414d Mon Sep 17 00:00:00 2001 From: "omegent-app[bot]" <306514130+omegent-app[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:54:32 +0000 Subject: [PATCH 1/3] feat(grok): allow mid-thread model switches and default to Grok 4.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grok has been the one provider that greys out its model picker once a conversation starts. That restriction dates from the commit that first added Grok over ACP (#2809) and was never revisited — but the rest of the code has long since disagreed with it: the adapter reports `sessionModelSwitch: "in-session"`, and `applyGrokAcpModelSelection` re-applies the requested model on every turn via `session/set_model`, which is also how picking a non-default model at thread start already works. Verified against the installed CLI (grok 1.0.3, `grok agent stdio`): after a completed turn, `session/set_model` to grok-4.5 returned OK and the next turn ran on the new model. `session/new` advertises the full `availableModels` list with a `currentModelId`, which is how an ACP agent signals it supports this. Grok's default model moves from Grok Build to Grok 4.6, so anything that picks Grok without naming a model — the composer, VS Code, the Discord bot when the operator pins the Grok instance — lands on the frontier model that Grok's own session default reports. The shared constant keeps the ACP fallback in step. The generic `requiresNewThreadForModelChange` mechanism stays: no provider uses it today, but it is upstream's and costs nothing to keep. Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com> --- .../src/provider/Layers/GrokProvider.test.ts | 4 +++- apps/server/src/provider/Layers/GrokProvider.ts | 5 ++++- .../server/src/provider/acp/GrokAcpSupport.test.ts | 14 ++++++++++++-- apps/server/src/provider/acp/GrokAcpSupport.ts | 5 +++-- packages/contracts/src/model.ts | 8 +++++++- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/apps/server/src/provider/Layers/GrokProvider.test.ts b/apps/server/src/provider/Layers/GrokProvider.test.ts index b9f80832cf4c..89ed96b829ef 100644 --- a/apps/server/src/provider/Layers/GrokProvider.test.ts +++ b/apps/server/src/provider/Layers/GrokProvider.test.ts @@ -84,7 +84,9 @@ describe("buildInitialGrokProviderSnapshot", () => { expect(snapshot.status).toBe("warning"); expect(snapshot.version).toBeNull(); expect(snapshot.message).toContain("Checking Grok"); - expect(snapshot.requiresNewThreadForModelChange).toBe(true); + // Grok switches models mid-session, so the snapshot must not carry the + // new-thread requirement that would grey out its model picker. + expect(snapshot.requiresNewThreadForModelChange).toBeUndefined(); const builtIn = snapshot.models.find((model) => model.slug === "grok-build"); expect( (builtIn?.capabilities?.optionDescriptors ?? []).some( diff --git a/apps/server/src/provider/Layers/GrokProvider.ts b/apps/server/src/provider/Layers/GrokProvider.ts index c0dfa9f18e83..d70ba320c1c6 100644 --- a/apps/server/src/provider/Layers/GrokProvider.ts +++ b/apps/server/src/provider/Layers/GrokProvider.ts @@ -33,11 +33,14 @@ import { } from "../providerMaintenance.ts"; import { makeGrokAcpRuntime, resolveGrokAcpBaseModelId } from "../acp/GrokAcpSupport.ts"; +// No `requiresNewThreadForModelChange`: Grok's ACP accepts `session/set_model` +// mid-session, and the adapter already re-applies the requested model on every +// turn (`applyGrokAcpModelSelection`). The flag dated from the original ACP +// integration, when that was not yet known to work. const GROK_PRESENTATION = { displayName: "Grok", badgeLabel: "Early Access", showInteractionModeToggle: true, - requiresNewThreadForModelChange: true, } as const; const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({ optionDescriptors: [], diff --git a/apps/server/src/provider/acp/GrokAcpSupport.test.ts b/apps/server/src/provider/acp/GrokAcpSupport.test.ts index c6241758aa17..7d480d730482 100644 --- a/apps/server/src/provider/acp/GrokAcpSupport.test.ts +++ b/apps/server/src/provider/acp/GrokAcpSupport.test.ts @@ -1,3 +1,8 @@ +import { + ProviderDriverKind, + DEFAULT_MODEL_BY_PROVIDER, + GROK_DEFAULT_MODEL, +} from "@t3tools/contracts"; import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as EffectAcpErrors from "effect-acp/errors"; @@ -14,10 +19,15 @@ import { describe("resolveGrokAcpBaseModelId", () => { it("normalizes empty and custom Grok model ids", () => { - expect(resolveGrokAcpBaseModelId(undefined)).toBe("grok-build"); - expect(resolveGrokAcpBaseModelId(" ")).toBe("grok-build"); + expect(resolveGrokAcpBaseModelId(undefined)).toBe(GROK_DEFAULT_MODEL); + expect(resolveGrokAcpBaseModelId(" ")).toBe(GROK_DEFAULT_MODEL); expect(resolveGrokAcpBaseModelId(" grok-test-custom-model ")).toBe("grok-test-custom-model"); }); + + it("falls back to the shared Grok default rather than Grok Build", () => { + expect(GROK_DEFAULT_MODEL).toBe("grok-4.6"); + expect(DEFAULT_MODEL_BY_PROVIDER[ProviderDriverKind.make("grok")]).toBe(GROK_DEFAULT_MODEL); + }); }); describe("buildGrokAcpSpawnInput", () => { diff --git a/apps/server/src/provider/acp/GrokAcpSupport.ts b/apps/server/src/provider/acp/GrokAcpSupport.ts index 8b59bbc02918..3fa9bf76e0ee 100644 --- a/apps/server/src/provider/acp/GrokAcpSupport.ts +++ b/apps/server/src/provider/acp/GrokAcpSupport.ts @@ -1,4 +1,5 @@ import { + GROK_DEFAULT_MODEL, type GrokSettings, type ModelSelection, type ProviderInteractionMode, @@ -150,8 +151,8 @@ export const makeGrokAcpRuntime = ( export function resolveGrokAcpBaseModelId(model: string | null | undefined): string { const trimmed = model?.trim(); - const base = trimmed && trimmed.length > 0 ? trimmed : "grok-build"; - return normalizeModelSlug(base, GROK_DRIVER_KIND) ?? "grok-build"; + const base = trimmed && trimmed.length > 0 ? trimmed : GROK_DEFAULT_MODEL; + return normalizeModelSlug(base, GROK_DRIVER_KIND) ?? GROK_DEFAULT_MODEL; } export function currentGrokModelIdFromSessionSetup( diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index 080fdd3d63c0..121055a71bca 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -136,6 +136,12 @@ const OPENCODE_DRIVER_KIND = ProviderDriverKind.make("opencode"); export const DEFAULT_MODEL = "gpt-5.6-sol"; +/** + * Grok's default model. Grok Build held this slot from the original Grok + * integration; 4.6 is the frontier model Grok's own session default reports. + */ +export const GROK_DEFAULT_MODEL = "grok-4.6"; + /** * Codex default-model preference, most preferred first. The provider snapshot * marks the first of these present in the live `model/list` response as @@ -152,7 +158,7 @@ export const DEFAULT_MODEL_BY_PROVIDER: Partial Date: Wed, 12 Aug 2026 21:05:46 +0000 Subject: [PATCH 2/3] fix(grok): make Grok 4.6 the model the picker actually defaults to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review found the default change was only half done. `getDefaultServerModel` prefers the provider catalog — a model marked `isDefault`, then the first non-custom entry — and only falls back to `DEFAULT_MODEL_BY_PROVIDER` when the catalog is empty. Grok's catalog never marked a default, so the new constant changed nothing in the picker. Discovered models now mark the model Grok itself reports as a fresh session's `currentModelId`, so the picker agrees with what a new thread would really run instead of trusting ACP list order. The built-in fallback catalog — what the picker shows until discovery answers, and whenever it fails — offered only `grok-build`. Grok 1.0.3 rejects that slug outright ("unknown model id"), so it could only produce sessions that failed to start. It is replaced by Grok 4.6 (default) and 4.5, the two models the CLI actually lists. Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com> --- .../src/provider/Layers/GrokProvider.test.ts | 39 +++++++++++++++++-- .../src/provider/Layers/GrokProvider.ts | 29 +++++++++++--- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/apps/server/src/provider/Layers/GrokProvider.test.ts b/apps/server/src/provider/Layers/GrokProvider.test.ts index 89ed96b829ef..8425b5e50773 100644 --- a/apps/server/src/provider/Layers/GrokProvider.test.ts +++ b/apps/server/src/provider/Layers/GrokProvider.test.ts @@ -4,10 +4,11 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; -import { GrokSettings } from "@t3tools/contracts"; +import { GROK_DEFAULT_MODEL, GrokSettings } from "@t3tools/contracts"; import { buildGrokCapabilitiesFromModelMeta, + buildGrokDiscoveredModelsFromSessionModelState, buildGrokReasoningEffortCapabilities, buildInitialGrokProviderSnapshot, checkGrokProviderStatus, @@ -87,12 +88,16 @@ describe("buildInitialGrokProviderSnapshot", () => { // Grok switches models mid-session, so the snapshot must not carry the // new-thread requirement that would grey out its model picker. expect(snapshot.requiresNewThreadForModelChange).toBeUndefined(); - const builtIn = snapshot.models.find((model) => model.slug === "grok-build"); + const builtIn = snapshot.models.find((model) => model.slug === GROK_DEFAULT_MODEL); expect( (builtIn?.capabilities?.optionDescriptors ?? []).some( (descriptor) => descriptor.id === "reasoningEffort", ), ).toBe(true); + // The picker default has to be a model the CLI still accepts: Grok 1.0.3 + // rejects the old `grok-build` slug outright. + expect(builtIn?.isDefault).toBe(true); + expect(snapshot.models.some((model) => model.slug === "grok-build")).toBe(false); }), ); }); @@ -164,8 +169,36 @@ it.layer(NodeServices.layer)("checkGrokProviderStatus", (it) => { expect(snapshot.status).toBe("error"); expect(snapshot.installed).toBe(true); - expect(snapshot.models.map((model) => model.slug)).toEqual(["grok-build"]); + expect(snapshot.models.map((model) => model.slug)).toEqual([GROK_DEFAULT_MODEL, "grok-4.5"]); expect(snapshot.message).toContain("ACP startup failed"); }), ); }); + +describe("buildGrokDiscoveredModelsFromSessionModelState", () => { + const modelState = (currentModelId: string | undefined) => ({ + ...(currentModelId === undefined ? {} : { currentModelId }), + availableModels: [ + { modelId: "grok-4.6", name: "Grok 4.6" }, + { modelId: "grok-4.5", name: "Grok 4.5" }, + ], + }); + + it("marks the model a fresh session starts on as the picker default", () => { + const models = buildGrokDiscoveredModelsFromSessionModelState(modelState("grok-4.5") as never); + + expect(models.map((model) => model.slug)).toEqual(["grok-4.6", "grok-4.5"]); + expect(models.find((model) => model.slug === "grok-4.5")?.isDefault).toBe(true); + expect(models.find((model) => model.slug === "grok-4.6")?.isDefault).toBeUndefined(); + }); + + it("leaves the default unmarked when Grok reports no current model", () => { + const models = buildGrokDiscoveredModelsFromSessionModelState(modelState(undefined) as never); + + expect(models.some((model) => model.isDefault)).toBe(false); + }); + + it("returns nothing when there is no model state to read", () => { + expect(buildGrokDiscoveredModelsFromSessionModelState(null)).toEqual([]); + }); +}); diff --git a/apps/server/src/provider/Layers/GrokProvider.ts b/apps/server/src/provider/Layers/GrokProvider.ts index d70ba320c1c6..6c9b4b6e0b91 100644 --- a/apps/server/src/provider/Layers/GrokProvider.ts +++ b/apps/server/src/provider/Layers/GrokProvider.ts @@ -1,4 +1,5 @@ import { + GROK_DEFAULT_MODEL, type GrokSettings, type ModelCapabilities, type ServerProvider, @@ -34,9 +35,8 @@ import { import { makeGrokAcpRuntime, resolveGrokAcpBaseModelId } from "../acp/GrokAcpSupport.ts"; // No `requiresNewThreadForModelChange`: Grok's ACP accepts `session/set_model` -// mid-session, and the adapter already re-applies the requested model on every -// turn (`applyGrokAcpModelSelection`). The flag dated from the original ACP -// integration, when that was not yet known to work. +// mid-session, and the adapter re-applies the requested model on every turn +// (`applyGrokAcpModelSelection`). const GROK_PRESENTATION = { displayName: "Grok", badgeLabel: "Early Access", @@ -77,10 +77,20 @@ export function buildGrokReasoningEffortCapabilities( const VERSION_PROBE_TIMEOUT_MS = 4_000; const GROK_ACP_MODEL_DISCOVERY_TIMEOUT_MS = 15_000; +// Shown until ACP model discovery answers (and whenever it fails). Grok 1.0.3 +// rejects the old `grok-build` slug with "unknown model id", so offering it +// here only produced sessions that could not start. const GROK_BUILT_IN_MODELS: ReadonlyArray = [ { - slug: "grok-build", - name: "Grok Build", + slug: GROK_DEFAULT_MODEL, + name: "Grok 4.6", + isCustom: false, + isDefault: true, + capabilities: buildGrokReasoningEffortCapabilities(GROK_FALLBACK_REASONING_EFFORTS), + }, + { + slug: "grok-4.5", + name: "Grok 4.5", isCustom: false, capabilities: buildGrokReasoningEffortCapabilities(GROK_FALLBACK_REASONING_EFFORTS), }, @@ -234,13 +244,19 @@ function grokModelsFromSettings( return providerModelsFromSettings(builtInModels, customModels ?? [], EMPTY_CAPABILITIES); } -function buildGrokDiscoveredModelsFromSessionModelState( +export function buildGrokDiscoveredModelsFromSessionModelState( modelState: EffectAcpSchema.SessionModelState | null | undefined, ): ReadonlyArray { if (!modelState || modelState.availableModels.length === 0) { return []; } const seen = new Set(); + // Grok reports which model a fresh session starts on; mark it default so the + // picker agrees with what a new thread would actually run, rather than + // whichever model happens to come first in the ACP list. + const currentSlug = modelState.currentModelId + ? resolveGrokAcpBaseModelId(modelState.currentModelId) + : undefined; return modelState.availableModels .map((model): ServerProviderModel | undefined => { const slug = resolveGrokAcpBaseModelId(model.modelId); @@ -252,6 +268,7 @@ function buildGrokDiscoveredModelsFromSessionModelState( slug, name: model.name.trim() || slug, isCustom: false, + ...(slug === currentSlug ? { isDefault: true } : {}), capabilities: buildGrokCapabilitiesFromModelMeta(model._meta), }; }) From 376a51ab84127ff2d6b9b1688b70cb8c3e368512 Mon Sep 17 00:00:00 2001 From: "omegent-app[bot]" <306514130+omegent-app[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:10:43 +0000 Subject: [PATCH 3/3] test(codex): keep PATH when overriding the launch-args environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `uses T3CODE_CODEX_LAUNCH_ARGS for codex exec over settings` passed the env override as the child's *entire* environment. That env is what the fake `codex` shell script runs with, so it lost PATH and could not resolve `cat`, producing no output and failing on "Codex returned invalid structured output". It survived on CI because /bin/sh falls back to a default PATH that finds /usr/bin/cat there; on a system without /bin/cat it has been failing for every local run, including the pre-push ship gate. Merging onto process.env keeps the assertion intact — the env still overrides the configured launch args — while leaving the script able to run. Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com> --- apps/server/src/textGeneration/CodexTextGeneration.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/server/src/textGeneration/CodexTextGeneration.test.ts b/apps/server/src/textGeneration/CodexTextGeneration.test.ts index 657118fff51c..537216bbd272 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.test.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.test.ts @@ -290,7 +290,10 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => { body: "", }), launchArgs: "--enable settings-feature", - environment: { T3CODE_CODEX_LAUNCH_ARGS: " --strict-config --listen off " }, + // Merged onto the real environment, not replacing it: this env is what + // the fake `codex` shell script is spawned with, and dropping PATH + // leaves it unable to resolve `cat` on systems without /bin/cat. + environment: { ...process.env, T3CODE_CODEX_LAUNCH_ARGS: " --strict-config --listen off " }, requireArg: "--strict-config", forbidArg: "settings-feature", },