diff --git a/apps/server/src/provider/testFixtures/grok-text-mock-agent.mjs b/apps/server/src/provider/testFixtures/grok-text-mock-agent.mjs index 357bdb7fe564d..b3c6e49ec6b4c 100644 --- a/apps/server/src/provider/testFixtures/grok-text-mock-agent.mjs +++ b/apps/server/src/provider/testFixtures/grok-text-mock-agent.mjs @@ -12,6 +12,17 @@ for await (const line of NodeReadline.createInterface({ input: process.stdin })) NodeFS.appendFileSync(process.env.T3_ACP_REQUEST_LOG_PATH, `${line}\n`); } if (request.id === undefined) continue; + if (request.method === "session/prompt" && process.env.T3_ACP_PROMPT_ERROR_MESSAGE) { + write({ + jsonrpc: "2.0", + id: request.id, + error: { + code: Number(process.env.T3_ACP_PROMPT_ERROR_CODE ?? -32003), + message: process.env.T3_ACP_PROMPT_ERROR_MESSAGE, + }, + }); + continue; + } let result; switch (request.method) { case "initialize": diff --git a/apps/server/src/textGeneration/GrokTextGeneration.test.ts b/apps/server/src/textGeneration/GrokTextGeneration.test.ts index 26ab9e79c7103..f91a06c627827 100644 --- a/apps/server/src/textGeneration/GrokTextGeneration.test.ts +++ b/apps/server/src/textGeneration/GrokTextGeneration.test.ts @@ -133,4 +133,58 @@ it.layer(layerGrokTextGenerationTest)("GrokTextGeneration", (it) => { }), ), ); + + it.effect("maps ACP failures to safe details and preserves their cause", () => + Effect.gen(function* () { + for (const [code, detail] of [ + [-32003, "Grok usage limit reached. Try again later."], + [-32000, "Grok authentication is required."], + [-32602, "Grok ACP request failed."], + ] as const) { + yield* withFakeAcpGrok( + { + T3_ACP_PROMPT_ERROR_MESSAGE: "private provider payload", + T3_ACP_PROMPT_ERROR_CODE: String(code), + }, + (textGeneration) => + Effect.gen(function* () { + const error = yield* textGeneration + .generateThreadTitle({ + cwd: process.cwd(), + message: "the lint job is red", + modelSelection: createModelSelection( + ProviderInstanceId.make("grok"), + "grok-mock-alt", + ), + }) + .pipe(Effect.flip); + expect(error.detail).toBe(detail); + expect(error.cause).toMatchObject({ + code, + errorMessage: "private provider payload", + }); + }), + ); + } + }), + ); + + it.effect("explains rejected model settings without copying provider text", () => + withFakeAcpGrok({}, (textGeneration) => + Effect.gen(function* () { + const error = yield* textGeneration + .generateThreadTitle({ + cwd: process.cwd(), + message: "the lint job is red", + modelSelection: createModelSelection( + ProviderInstanceId.make("grok"), + "grok-mock-missing", + ), + }) + .pipe(Effect.flip); + expect(error.cause).toMatchObject({ code: -32602, errorMessage: "Unknown Grok model" }); + expect(error.detail).toBe("Grok rejected the text generation model settings."); + }), + ), + ); }); diff --git a/packages/provider-grok/src/server/textGeneration.ts b/packages/provider-grok/src/server/textGeneration.ts index 61a6b2d482f53..1b9ede6652e85 100644 --- a/packages/provider-grok/src/server/textGeneration.ts +++ b/packages/provider-grok/src/server/textGeneration.ts @@ -4,7 +4,7 @@ import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; -import type * as EffectAcpErrors from "effect-acp/errors"; +import * as EffectAcpErrors from "effect-acp/errors"; import { TextGenerationError } from "@t3tools/contracts"; import type { GrokSettings } from "../settings.ts"; @@ -18,10 +18,25 @@ import { makeGrokAcpRuntime, resolveGrokAcpBaseModelId, } from "./acpSupport.ts"; +import { xAiRateLimitedErrorCode } from "./xaiAcpExtension.ts"; const GROK_TIMEOUT_MS = 180_000; const isTextGenerationError = Schema.is(TextGenerationError); +const isAcpRequestError = Schema.is(EffectAcpErrors.AcpRequestError); + +function grokAcpFailureDetail(cause: EffectAcpErrors.AcpError, fallback: string) { + if (!isAcpRequestError(cause)) return fallback; + switch (cause.code) { + case xAiRateLimitedErrorCode: + return "Grok usage limit reached. Try again later."; + // ACP auth_required + case -32000: + return "Grok authentication is required."; + default: + return fallback; + } +} export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(function* ( grokSettings: GrokSettings, @@ -75,7 +90,12 @@ export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(functi mapError: (cause) => new TextGenerationError({ operation, - detail: "Failed to set Grok ACP base model for text generation.", + detail: grokAcpFailureDetail( + cause, + isAcpRequestError(cause) && cause.code === -32602 + ? "Grok rejected the text generation model settings." + : "Failed to set Grok ACP base model for text generation.", + ), cause, }), }); @@ -98,7 +118,7 @@ export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(functi ? cause : new TextGenerationError({ operation, - detail: "Grok ACP request failed.", + detail: grokAcpFailureDetail(cause, "Grok ACP request failed."), cause, }), ),