import { type GrokSettings, ProviderDriverKind, type RuntimeMode } from "@t3tools/contracts"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Scope from "effect/Scope"; import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; import * as EffectAcpErrors from "effect-acp/errors"; import type * as EffectAcpSchema from "effect-acp/schema"; import { normalizeModelSlug } from "@t3tools/shared/model"; import * as AcpSessionRuntime from "./AcpSessionRuntime.ts"; import { makeXAiPromptCompletionRuntime } from "./XAiAcpExtension.ts"; const GROK_API_KEY_ENV = "XAI_API_KEY"; const GROK_OAUTH2_REFERRER_ENV = "GROK_OAUTH2_REFERRER"; const T3_CODE_OAUTH_REFERRER = "t3code"; const GROK_AUTH_METHOD_API_KEY = "xai.api_key"; const GROK_AUTH_METHOD_CACHED_TOKEN = "cached_token"; const GROK_DRIVER_KIND = ProviderDriverKind.make("grok"); type GrokAcpRuntimeGrokSettings = Pick; interface GrokAcpRuntimeInput extends Omit< AcpSessionRuntime.AcpSessionRuntimeOptions, "authMethodId" | "clientCapabilities" | "spawn" > { readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"]; readonly grokSettings: GrokAcpRuntimeGrokSettings | null | undefined; readonly environment?: NodeJS.ProcessEnv; readonly runtimeMode?: RuntimeMode; } export function grokAcpSpawnArgs(runtimeMode?: RuntimeMode): ReadonlyArray { switch (runtimeMode) { case "approval-required": return ["--permission-mode", "default", "agent", "stdio"]; case "auto-accept-edits": return ["--permission-mode", "acceptEdits", "agent", "stdio"]; case "auto": return ["--permission-mode", "auto", "agent", "stdio"]; case "full-access": return ["agent", "--always-approve", "stdio"]; default: return ["agent", "stdio"]; } } export function buildGrokAcpSpawnInput( grokSettings: GrokAcpRuntimeGrokSettings | null | undefined, cwd: string, environment?: NodeJS.ProcessEnv, runtimeMode?: RuntimeMode, ): AcpSessionRuntime.AcpSpawnInput { return { command: grokSettings?.binaryPath || "grok", args: [...grokAcpSpawnArgs(runtimeMode)], cwd, env: { ...environment, [GROK_OAUTH2_REFERRER_ENV]: T3_CODE_OAUTH_REFERRER, }, }; } function resolveGrokAuthMethodId(environment: NodeJS.ProcessEnv | undefined): string { return environment?.[GROK_API_KEY_ENV]?.trim() ? GROK_AUTH_METHOD_API_KEY : GROK_AUTH_METHOD_CACHED_TOKEN; } export const makeGrokAcpRuntime = ( input: GrokAcpRuntimeInput, ): Effect.Effect< AcpSessionRuntime.AcpSessionRuntime["Service"], EffectAcpErrors.AcpError, Crypto.Crypto | Scope.Scope > => Effect.gen(function* () { const acpContext = yield* Layer.build( AcpSessionRuntime.layer({ ...input, spawn: buildGrokAcpSpawnInput( input.grokSettings, input.cwd, input.environment, input.runtimeMode, ), authMethodId: resolveGrokAuthMethodId(input.environment), }).pipe( Layer.provide( Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, input.childProcessSpawner), ), ), ); const runtime = yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe( Effect.provide(acpContext), ); return yield* makeXAiPromptCompletionRuntime(runtime); }); /** * T3's built-in Grok slug. It is the CLI's product name, not a model id the ACP accepts, * so selecting it means "use whatever model the Grok session currently runs on". */ export const GROK_DEFAULT_MODEL_SLUG = "grok-build"; export function resolveGrokAcpBaseModelId(model: string | null | undefined): string { const trimmed = model?.trim(); const base = trimmed && trimmed.length > 0 ? trimmed : GROK_DEFAULT_MODEL_SLUG; return normalizeModelSlug(base, GROK_DRIVER_KIND) ?? GROK_DEFAULT_MODEL_SLUG; } const GROK_REASONING_EFFORT_TOKEN = /^[a-z0-9][a-z0-9._-]{0,31}$/i; export function isValidGrokReasoningEffortToken(value: string): boolean { return GROK_REASONING_EFFORT_TOKEN.test(value); } export function normalizeGrokReasoningEffort(value: string | undefined): string | undefined { const effort = value?.trim(); return effort && isValidGrokReasoningEffortToken(effort) ? effort : undefined; } export function currentGrokModelIdFromSessionSetup( sessionSetupResult: | EffectAcpSchema.LoadSessionResponse | EffectAcpSchema.NewSessionResponse | EffectAcpSchema.ResumeSessionResponse, ): string | undefined { return sessionSetupResult.models?.currentModelId?.trim() || undefined; } export function currentGrokReasoningEffortFromSessionSetup( sessionSetupResult: | EffectAcpSchema.LoadSessionResponse | EffectAcpSchema.NewSessionResponse | EffectAcpSchema.ResumeSessionResponse, ): string | undefined { const modelState = sessionSetupResult.models; if (!modelState) { return undefined; } const currentModelId = modelState.currentModelId.trim(); if (currentModelId.length === 0) { return undefined; } const currentModel = modelState.availableModels.find( (model) => model.modelId.trim() === currentModelId, ); const reasoningEffort = currentModel?._meta?.reasoningEffort; return typeof reasoningEffort === "string" ? normalizeGrokReasoningEffort(reasoningEffort) : undefined; } export function applyGrokAcpModelSelection(input: { readonly runtime: Pick; readonly currentModelId: string | undefined; readonly currentReasoningEffort?: string | undefined; readonly requestedModelId: string | undefined; readonly requestedReasoningEffort?: string | undefined; readonly mapError: (cause: EffectAcpErrors.AcpError) => E; }): Effect.Effect { // The product slug is never sent over the wire; it keeps the session's current model. const requestedModelId = input.requestedModelId === GROK_DEFAULT_MODEL_SLUG ? undefined : input.requestedModelId; const modelChanged = requestedModelId !== undefined && requestedModelId !== input.currentModelId; const reasoningProvided = input.requestedReasoningEffort !== undefined; const reasoningEffort = reasoningProvided ? normalizeGrokReasoningEffort(input.requestedReasoningEffort) : undefined; const reasoningEffortChanged = reasoningProvided && reasoningEffort !== input.currentReasoningEffort; const targetModelId = requestedModelId ?? input.currentModelId; if ((!modelChanged && !reasoningEffortChanged) || targetModelId === undefined) { return Effect.succeed(input.currentModelId); } const reasoningMeta = reasoningProvided && reasoningEffort !== undefined ? { reasoningEffort } : undefined; // When reasoning was explicitly provided but invalid (normalize => undefined), we deliberately // send no meta so the invalid value is dropped rather than forwarded. When reasoning was not // provided at all, we also send no meta, but we only reach this call when the model itself // changed - an omitted reasoning preference must not be treated as an explicit clear of the // CLI-advertised default (e.g. Extra High) on same-model reselections. return input.runtime .setSessionModel(targetModelId, reasoningMeta) .pipe(Effect.mapError(input.mapError), Effect.as(targetModelId)); }