From 13f532ba014b33962ad3100559cf7ddb1d327cbd Mon Sep 17 00:00:00 2001 From: Geezy Date: Fri, 6 Nov 2026 11:15:35 +0100 Subject: [PATCH 1/2] fix(pi): text generation errors show the HTTP status from Gemini and Mistral Pi's errorMessage is the provider SDK's raw error text, and only a leading status was recognized. Pi's Gemini provider reports the response body as JSON with the status in `error.code`, and its Mistral provider writes "Mistral API error (429): ...", so both fell back to "the model provider reported an error". Read the status from those two forms as well. Still only the status reaches the error detail. Co-Authored-By: Claude Opus 5.5 --- .../src/server/textGeneration.test.ts | 18 ++++++++++++++++++ .../provider-pi/src/server/textGeneration.ts | 9 +++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/provider-pi/src/server/textGeneration.test.ts b/packages/provider-pi/src/server/textGeneration.test.ts index 0e6a8a3c07780..6f3fa61598729 100644 --- a/packages/provider-pi/src/server/textGeneration.test.ts +++ b/packages/provider-pi/src/server/textGeneration.test.ts @@ -169,6 +169,24 @@ it.effect("keeps Pi's raw error text out of the error", () => }), ); +it.effect("reports the HTTP status of Gemini and Mistral errors", () => + Effect.gen(function* () { + // Pi's errorMessage for a 429 from its Gemini and Mistral providers. + for (const errorMessage of [ + '{"error":{"code":429,"message":"Quota exceeded for key AIzaSECRET123","status":"RESOURCE_EXHAUSTED"}}', + 'Mistral API error (429): {"object":"error","message":"Rate limit exceeded for SECRET123"}', + ]) { + const error = yield* generateCommitMessage([failedTurnWith(errorMessage)]).pipe(Effect.flip); + + assert.equal( + error.detail, + "Pi could not generate text: the model provider returned HTTP 429.", + ); + assert.notInclude(error.message, "SECRET123"); + } + }), +); + it.effect("keeps the reply when Pi answers after a failed attempt", () => Effect.gen(function* () { const result = yield* generateCommitMessage( diff --git a/packages/provider-pi/src/server/textGeneration.ts b/packages/provider-pi/src/server/textGeneration.ts index 40697f2d4280a..e565c0f2e5c94 100644 --- a/packages/provider-pi/src/server/textGeneration.ts +++ b/packages/provider-pi/src/server/textGeneration.ts @@ -16,9 +16,14 @@ const PI_TIMEOUT_MS = 180_000; const isTextGenerationError = Schema.is(TextGenerationError); // Pi's `errorMessage` is the provider's raw response text and can echo -// credentials, so only a leading HTTP status reaches the error detail. +// credentials, so only its HTTP status reaches the error detail. Most SDKs lead +// with it; Mistral's follows "Mistral API error (", and Gemini's is the body's `error.code`. +const PI_ERROR_STATUS = + /^(?:Mistral API error \()?([45]\d\d)\b|^\{"error":\{.*?"code":([45]\d\d)[,}]/; + const piTurnErrorDetail = (errorMessage: string | undefined) => { - const status = errorMessage?.match(/^([45]\d\d)\b/)?.[1]; + const match = errorMessage?.match(PI_ERROR_STATUS); + const status = match?.[1] ?? match?.[2]; return status === undefined ? "Pi could not generate text: the model provider reported an error." : `Pi could not generate text: the model provider returned HTTP ${status}.`; From bedb5554c18896773351ecaf616b7221a34d0aae Mon Sep 17 00:00:00 2001 From: Geezy Date: Fri, 6 Nov 2026 11:43:35 +0100 Subject: [PATCH 2/2] test(pi): fold the Gemini and Mistral status cases into the existing test Co-Authored-By: Claude Opus 5.5 --- .../src/server/textGeneration.test.ts | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/provider-pi/src/server/textGeneration.test.ts b/packages/provider-pi/src/server/textGeneration.test.ts index 6f3fa61598729..e1b6c682b7f85 100644 --- a/packages/provider-pi/src/server/textGeneration.test.ts +++ b/packages/provider-pi/src/server/textGeneration.test.ts @@ -155,38 +155,36 @@ it.effect("reports Pi's turn error when the reply is empty", () => assert.equal(error._tag, "TextGenerationError"); assert.equal(error.detail, "Pi could not generate text: the model provider returned HTTP 401."); - }), -); - -it.effect("keeps Pi's raw error text out of the error", () => - Effect.gen(function* () { - const error = yield* generateCommitMessage([ - failedTurnWith("Invalid API key sk-test-SECRET123"), - ]).pipe(Effect.flip); - - assert.equal(error.detail, "Pi could not generate text: the model provider reported an error."); - assert.notInclude(error.message, "SECRET123"); - }), -); -it.effect("reports the HTTP status of Gemini and Mistral errors", () => - Effect.gen(function* () { // Pi's errorMessage for a 429 from its Gemini and Mistral providers. for (const errorMessage of [ '{"error":{"code":429,"message":"Quota exceeded for key AIzaSECRET123","status":"RESOURCE_EXHAUSTED"}}', 'Mistral API error (429): {"object":"error","message":"Rate limit exceeded for SECRET123"}', ]) { - const error = yield* generateCommitMessage([failedTurnWith(errorMessage)]).pipe(Effect.flip); + const providerError = yield* generateCommitMessage([failedTurnWith(errorMessage)]).pipe( + Effect.flip, + ); assert.equal( - error.detail, + providerError.detail, "Pi could not generate text: the model provider returned HTTP 429.", ); - assert.notInclude(error.message, "SECRET123"); + assert.notInclude(providerError.message, "SECRET123"); } }), ); +it.effect("keeps Pi's raw error text out of the error", () => + Effect.gen(function* () { + const error = yield* generateCommitMessage([ + failedTurnWith("Invalid API key sk-test-SECRET123"), + ]).pipe(Effect.flip); + + assert.equal(error.detail, "Pi could not generate text: the model provider reported an error."); + assert.notInclude(error.message, "SECRET123"); + }), +); + it.effect("keeps the reply when Pi answers after a failed attempt", () => Effect.gen(function* () { const result = yield* generateCommitMessage(