fix(ai): retry Mistral finish_reason error

Mistral reports transient server failures as finish_reason "error". Its
error message now includes "server error" so the retry classifier treats
it as retryable.

closes #10487
This commit is contained in:
Mario Zechner 2026-10-07 09:06:19 +00:00
parent b2363841a5
commit 7fb59f995b
3 changed files with 8 additions and 2 deletions

View file

@ -25,6 +25,7 @@
- Fixed Radius models disabled by an organization owner still being listed: a fetched or cached Radius catalog now replaces the shipped default catalog instead of being merged into it
- Fixed OpenAI provider type-checking with cached catalogs that contain no classifier models
- Fixed Anthropic browser login failing with "localhost refused to connect" when port 53692 is reserved or in use, for example by Hyper-V/WSL port exclusions on Windows: login now falls back to a free loopback port ([#10571](https://github.com/earendil-works/pi/issues/10571))
- Fixed Mistral responses that end with `finish_reason: "error"` not being retried ([#10487](https://github.com/earendil-works/pi/issues/10487))
## [1.0.4] - 2026-10-05

View file

@ -930,7 +930,8 @@ function mapChatStopReason(reason: string | null): { stopReason: StopReason; err
case "tool_calls":
return { stopReason: "toolUse" };
case "error":
return { stopReason: "error", errorMessage: "Provider stopped with: error" };
// Mistral reports transient server failures this way; "server error" makes the message retryable.
return { stopReason: "error", errorMessage: "Provider stopped with: error (server error)" };
default:
return { stopReason: "error", errorMessage: `Provider stopped with: ${reason}` };
}

View file

@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { stream as streamMistral } from "../src/api/mistral-conversations.ts";
import { getModel, normalizeContext } from "../src/compat.ts";
import type { FetchFunction } from "../src/types.ts";
import { isRetryableAssistantError } from "../src/utils/retry.ts";
const model = getModel("mistral", "devstral-medium-latest");
const context = normalizeContext({
@ -51,7 +52,9 @@ describe("Mistral raw stop reasons", () => {
expect(message.stopReason).toBe("error");
expect(message.rawStopReason).toBe("error");
expect(message.errorMessage).toBe("Provider stopped with: error");
expect(message.errorMessage).toBe("Provider stopped with: error (server error)");
// #10487
expect(isRetryableAssistantError(message)).toBe(true);
});
it("treats unknown Mistral finish reasons as provider error stops", async () => {
@ -63,5 +66,6 @@ describe("Mistral raw stop reasons", () => {
expect(message.stopReason).toBe("error");
expect(message.rawStopReason).toBe("unmapped_error");
expect(message.errorMessage).toBe("Provider stopped with: unmapped_error");
expect(isRetryableAssistantError(message)).toBe(false);
});
});