Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions apps/server/src/provider/Layers/GrokProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -84,13 +85,19 @@ describe("buildInitialGrokProviderSnapshot", () => {
expect(snapshot.status).toBe("warning");
expect(snapshot.version).toBeNull();
expect(snapshot.message).toContain("Checking Grok");
expect(snapshot.requiresNewThreadForModelChange).toBe(true);
const builtIn = snapshot.models.find((model) => model.slug === "grok-build");
// 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_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);
}),
);
});
Expand Down Expand Up @@ -162,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([]);
});
});
28 changes: 24 additions & 4 deletions apps/server/src/provider/Layers/GrokProvider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
GROK_DEFAULT_MODEL,
type GrokSettings,
type ModelCapabilities,
type ServerProvider,
Expand Down Expand Up @@ -33,11 +34,13 @@ 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 re-applies the requested model on every turn
// (`applyGrokAcpModelSelection`).
const GROK_PRESENTATION = {
displayName: "Grok",
badgeLabel: "Early Access",
showInteractionModeToggle: true,
requiresNewThreadForModelChange: true,
} as const;
const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({
optionDescriptors: [],
Expand Down Expand Up @@ -74,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<ServerProviderModel> = [
{
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),
},
Expand Down Expand Up @@ -231,13 +244,19 @@ function grokModelsFromSettings(
return providerModelsFromSettings(builtInModels, customModels ?? [], EMPTY_CAPABILITIES);
}

function buildGrokDiscoveredModelsFromSessionModelState(
export function buildGrokDiscoveredModelsFromSessionModelState(
modelState: EffectAcpSchema.SessionModelState | null | undefined,
): ReadonlyArray<ServerProviderModel> {
if (!modelState || modelState.availableModels.length === 0) {
return [];
}
const seen = new Set<string>();
// 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);
Expand All @@ -249,6 +268,7 @@ function buildGrokDiscoveredModelsFromSessionModelState(
slug,
name: model.name.trim() || slug,
isCustom: false,
...(slug === currentSlug ? { isDefault: true } : {}),
capabilities: buildGrokCapabilitiesFromModelMeta(model._meta),
};
})
Expand Down
14 changes: 12 additions & 2 deletions apps/server/src/provider/acp/GrokAcpSupport.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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", () => {
Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/provider/acp/GrokAcpSupport.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
GROK_DEFAULT_MODEL,
type GrokSettings,
type ModelSelection,
type ProviderInteractionMode,
Expand Down Expand Up @@ -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(
Expand Down
5 changes: 4 additions & 1 deletion apps/server/src/textGeneration/CodexTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down
8 changes: 7 additions & 1 deletion packages/contracts/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -152,7 +158,7 @@ export const DEFAULT_MODEL_BY_PROVIDER: Partial<Record<ProviderDriverKind, strin
[CODEX_DRIVER_KIND]: DEFAULT_MODEL,
[CLAUDE_DRIVER_KIND]: "claude-opus-4-8",
[CURSOR_DRIVER_KIND]: "auto",
[GROK_DRIVER_KIND]: "grok-build",
[GROK_DRIVER_KIND]: GROK_DEFAULT_MODEL,
[KIMI_DRIVER_KIND]: "kimi-code/k3",
[OPENCODE_DRIVER_KIND]: "openai/gpt-5",
};
Expand Down
Loading