Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/server/src/provider/Layers/GrokAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
runtime: acp,
currentModelId: currentGrokModelIdFromSessionSetup(started.sessionSetupResult),
requestedModelId: requestedStartModelId,
selections: grokModelSelection?.options,
mapError: (cause) =>
mapAcpToAdapterError(PROVIDER, input.threadId, "session/set_model", cause),
});
Expand Down Expand Up @@ -946,6 +947,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
runtime: ctx.acp,
currentModelId: ctx.currentModelId,
requestedModelId: requestedTurnModelId,
selections: turnModelSelection?.options,
mapError: (cause) =>
mapAcpToAdapterError(PROVIDER, input.threadId, "session/set_model", cause),
});
Expand Down
103 changes: 102 additions & 1 deletion apps/server/src/provider/Layers/GrokProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,111 @@ import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import { GrokSettings } from "@t3tools/contracts";

import { buildInitialGrokProviderSnapshot, checkGrokProviderStatus } from "./GrokProvider.ts";
import {
buildGrokCapabilitiesFromModelMeta,
buildGrokDiscoveredModelsFromSessionModelState,
buildInitialGrokProviderSnapshot,
checkGrokProviderStatus,
} from "./GrokProvider.ts";

const decodeGrokSettings = Schema.decodeSync(GrokSettings);

describe("buildGrokCapabilitiesFromModelMeta", () => {
it("maps reasoningEfforts meta into a Reasoning select descriptor", () => {
// Opaque fixture values — not coupled to any live Grok catalog/menu.
const caps = buildGrokCapabilitiesFromModelMeta({
supportsReasoningEffort: true,
reasoningEffort: "effort-b",
reasoningEfforts: [
{ id: "effort-a", value: "effort-a", label: "Effort A", default: true },
{ id: "effort-b", value: "effort-b", label: "Effort B", default: false },
{ id: "effort-c", value: "effort-c", label: "Effort C", default: false },
],
});

expect(caps.optionDescriptors).toEqual([
{
id: "reasoningEffort",
label: "Reasoning",
type: "select",
currentValue: "effort-b",
options: [
{ id: "effort-a", label: "Effort A" },
{ id: "effort-b", label: "Effort B", isDefault: true },
{ id: "effort-c", label: "Effort C" },
],
},
]);
});

it("keeps menus independent per model meta", () => {
const caps = buildGrokCapabilitiesFromModelMeta({
supportsReasoningEffort: true,
reasoningEffort: "only",
reasoningEfforts: [{ id: "only", value: "only", label: "Only", default: false }],
});
expect(caps.optionDescriptors?.[0]?.type).toBe("select");
if (caps.optionDescriptors?.[0]?.type === "select") {
expect(caps.optionDescriptors[0].options.map((option) => option.id)).toEqual(["only"]);
expect(caps.optionDescriptors[0].currentValue).toBe("only");
}
});

it("returns empty capabilities when meta has no efforts", () => {
expect(buildGrokCapabilitiesFromModelMeta(undefined).optionDescriptors).toEqual([]);
expect(buildGrokCapabilitiesFromModelMeta({}).optionDescriptors).toEqual([]);
expect(
buildGrokCapabilitiesFromModelMeta({ supportsReasoningEffort: true }).optionDescriptors,
).toEqual([]);
});
});

describe("buildGrokDiscoveredModelsFromSessionModelState", () => {
it("attaches per-model capabilities from each entry's _meta", () => {
// Pure fixtures — slugs/menus are invented so catalog renames cannot break this.
const models = buildGrokDiscoveredModelsFromSessionModelState({
currentModelId: "model-a",
availableModels: [
{
modelId: "model-a",
name: "Model A",
_meta: {
reasoningEffort: "effort-b",
reasoningEfforts: [
{ value: "effort-a", label: "Effort A", default: true },
{ value: "effort-b", label: "Effort B", default: false },
{ value: "effort-c", label: "Effort C", default: false },
],
},
},
{
modelId: "model-b",
name: "Model B",
_meta: {
reasoningEffort: "only",
reasoningEfforts: [{ value: "only", label: "Only" }],
},
},
],
});

expect(models.map((model) => model.slug)).toEqual(["model-a", "model-b"]);
expect(models[0]?.capabilities.optionDescriptors?.[0]).toMatchObject({
id: "reasoningEffort",
currentValue: "effort-b",
});
expect(models[1]?.capabilities.optionDescriptors?.[0]).toMatchObject({
id: "reasoningEffort",
currentValue: "only",
});
if (models[1]?.capabilities.optionDescriptors?.[0]?.type === "select") {
expect(models[1].capabilities.optionDescriptors[0].options.map((o) => o.id)).toEqual([
"only",
]);
}
});
});

describe("buildInitialGrokProviderSnapshot", () => {
it.effect("returns a disabled snapshot when settings.enabled is false", () =>
Effect.gen(function* () {
Expand Down
79 changes: 76 additions & 3 deletions apps/server/src/provider/Layers/GrokProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { createModelCapabilities } from "@t3tools/shared/model";
import { resolveSpawnCommand } from "@t3tools/shared/shell";

import {
buildSelectOptionDescriptor,
buildServerProvider,
isCommandMissingCause,
parseGenericCliVersion,
Expand All @@ -29,7 +30,11 @@ import {
enrichProviderSnapshotWithVersionAdvisory,
type ProviderMaintenanceCapabilities,
} from "../providerMaintenance.ts";
import { makeGrokAcpRuntime, resolveGrokAcpBaseModelId } from "../acp/GrokAcpSupport.ts";
import {
GROK_REASONING_EFFORT_OPTION_ID,
makeGrokAcpRuntime,
resolveGrokAcpBaseModelId,
} from "../acp/GrokAcpSupport.ts";

const GROK_PRESENTATION = {
displayName: "Grok",
Expand All @@ -53,6 +58,74 @@ const GROK_BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
},
];

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

/**
* Build model capabilities from Grok ACP model `_meta`.
* Effort menus are per-model (built-in and custom) via `reasoningEfforts`.
*/
export function buildGrokCapabilitiesFromModelMeta(meta: unknown): ModelCapabilities {
if (!isRecord(meta)) {
return EMPTY_CAPABILITIES;
}

const effortsRaw = meta.reasoningEfforts;
if (!Array.isArray(effortsRaw) || effortsRaw.length === 0) {
return EMPTY_CAPABILITIES;
}

const currentEffort =
typeof meta.reasoningEffort === "string" ? meta.reasoningEffort.trim() : undefined;

const parsed = effortsRaw.flatMap((entry) => {
if (!isRecord(entry)) {
return [];
}
const valueRaw =
typeof entry.value === "string" ? entry.value : typeof entry.id === "string" ? entry.id : "";
const value = valueRaw.trim();
if (!value) {
return [];
}
const label =
typeof entry.label === "string" && entry.label.trim().length > 0 ? entry.label.trim() : value;
return [
{
value,
label,
markedDefault: entry.default === true,
},
];
});

if (parsed.length === 0) {
return EMPTY_CAPABILITIES;
}

const preferredDefault =
(currentEffort && parsed.some((option) => option.value === currentEffort)
? currentEffort
: undefined) ??
parsed.find((option) => option.markedDefault)?.value ??
parsed[0]?.value;

return createModelCapabilities({
optionDescriptors: [
buildSelectOptionDescriptor({
id: GROK_REASONING_EFFORT_OPTION_ID,
label: "Reasoning",
options: parsed.map((option) => ({
value: option.value,
label: option.label,
...(option.value === preferredDefault ? { isDefault: true as const } : {}),
})),
}),
],
});
}

export function buildInitialGrokProviderSnapshot(
grokSettings: GrokSettings,
): Effect.Effect<ServerProviderDraft> {
Expand Down Expand Up @@ -99,7 +172,7 @@ 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) {
Expand All @@ -117,7 +190,7 @@ function buildGrokDiscoveredModelsFromSessionModelState(
slug,
name: model.name.trim() || slug,
isCustom: false,
capabilities: EMPTY_CAPABILITIES,
capabilities: buildGrokCapabilitiesFromModelMeta(model._meta),
};
})
.filter((model): model is ServerProviderModel => model !== undefined);
Expand Down
7 changes: 6 additions & 1 deletion apps/server/src/provider/acp/AcpSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,14 @@ export class AcpSessionRuntime extends Context.Service<
readonly setModel: (model: string) => Effect.Effect<void, EffectAcpErrors.AcpError>;
/**
* Selects the active model through the unstable ACP `session/set_model` capability.
* Optional `_meta` is passed through for agent extensions (e.g. Grok `reasoningEffort`).
* @see https://agentclientprotocol.com/protocol/schema#session/set_model
*/
readonly setSessionModel: (
modelId: string,
options?: {
readonly _meta?: { readonly [x: string]: unknown } | null;
},
) => Effect.Effect<EffectAcpSchema.SetSessionModelResponse, EffectAcpErrors.AcpError>;
/**
* Sends a generic ACP extension request and records it through the request logger.
Expand Down Expand Up @@ -789,12 +793,13 @@ export const make = (
Effect.flatMap((started) => setConfigOption(started.modelConfigId ?? "model", model)),
Effect.asVoid,
),
setSessionModel: (modelId) =>
setSessionModel: (modelId, options) =>
getStartedState.pipe(
Effect.flatMap((started) => {
const requestPayload = {
sessionId: started.sessionId,
modelId,
...(options?._meta !== undefined ? { _meta: options._meta } : {}),
} satisfies EffectAcpSchema.SetSessionModelRequest;
return runLoggedRequest(
"session/set_model",
Expand Down
78 changes: 62 additions & 16 deletions apps/server/src/provider/acp/GrokAcpSupport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,17 @@ describe("buildGrokAcpSpawnInput", () => {

describe("applyGrokAcpModelSelection", () => {
const makeRecordingRuntime = (failure?: EffectAcpErrors.AcpError) => {
const modelCalls: Array<string> = [];
const modelCalls: Array<{
modelId: string;
options?: { readonly _meta?: { readonly [x: string]: unknown } | null };
}> = [];
const runtime = {
setSessionModel: (modelId: string) =>
setSessionModel: (
modelId: string,
options?: { readonly _meta?: { readonly [x: string]: unknown } | null },
) =>
Effect.gen(function* () {
modelCalls.push(modelId);
modelCalls.push({ modelId, ...(options ? { options } : {}) });
if (failure) return yield* failure;
return {};
}),
Expand All @@ -54,40 +60,80 @@ describe("applyGrokAcpModelSelection", () => {
const { runtime, modelCalls } = makeRecordingRuntime();
const result = yield* applyGrokAcpModelSelection({
runtime,
currentModelId: "grok-build",
requestedModelId: "grok-mock-alt",
currentModelId: "model-a",
requestedModelId: "model-b",
mapError: (cause) => cause.message,
});
expect(modelCalls).toEqual(["grok-mock-alt"]);
expect(result).toBe("grok-mock-alt");
expect(modelCalls).toEqual([{ modelId: "model-b" }]);
expect(result).toBe("model-b");
}),
);

it.effect("skips set_model when requested matches current", () =>
it.effect("skips set_model when requested matches current and no effort is selected", () =>
Effect.gen(function* () {
const { runtime, modelCalls } = makeRecordingRuntime();
const result = yield* applyGrokAcpModelSelection({
runtime,
currentModelId: "grok-build",
requestedModelId: "grok-build",
currentModelId: "model-a",
requestedModelId: "model-a",
mapError: (cause) => cause.message,
});
expect(modelCalls).toEqual([]);
expect(result).toBe("grok-build");
expect(result).toBe("model-a");
}),
);

it.effect("skips set_model when no model is requested", () =>
it.effect("skips set_model when no model is requested and no effort is selected", () =>
Effect.gen(function* () {
const { runtime, modelCalls } = makeRecordingRuntime();
const result = yield* applyGrokAcpModelSelection({
runtime,
currentModelId: "grok-build",
currentModelId: "model-a",
requestedModelId: undefined,
mapError: (cause) => cause.message,
});
expect(modelCalls).toEqual([]);
expect(result).toBe("grok-build");
expect(result).toBe("model-a");
}),
);

it.effect("applies reasoning effort via set_model _meta without changing model id", () =>
Effect.gen(function* () {
const { runtime, modelCalls } = makeRecordingRuntime();
const result = yield* applyGrokAcpModelSelection({
runtime,
currentModelId: "model-a",
requestedModelId: "model-a",
selections: [{ id: "reasoningEffort", value: "effort-c" }],
mapError: (cause) => cause.message,
});
expect(modelCalls).toEqual([
{
modelId: "model-a",
options: { _meta: { reasoningEffort: "effort-c" } },
},
]);
expect(result).toBe("model-a");
}),
);

it.effect("applies model switch and effort together", () =>
Effect.gen(function* () {
const { runtime, modelCalls } = makeRecordingRuntime();
const result = yield* applyGrokAcpModelSelection({
runtime,
currentModelId: "model-a",
requestedModelId: "model-b",
selections: [{ id: "reasoningEffort", value: "only" }],
mapError: (cause) => cause.message,
});
expect(modelCalls).toEqual([
{
modelId: "model-b",
options: { _meta: { reasoningEffort: "only" } },
},
]);
expect(result).toBe("model-b");
}),
);

Expand All @@ -98,8 +144,8 @@ describe("applyGrokAcpModelSelection", () => {
const error = yield* Effect.flip(
applyGrokAcpModelSelection({
runtime,
currentModelId: "grok-build",
requestedModelId: "grok-mock-alt",
currentModelId: "model-a",
requestedModelId: "model-b",
mapError: (cause) => cause.message,
}),
);
Expand Down
Loading
Loading