diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index adcf2b773..abb9514bc 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -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 diff --git a/packages/ai/src/api/mistral-conversations.ts b/packages/ai/src/api/mistral-conversations.ts index a6a5b2564..de7552ad6 100644 --- a/packages/ai/src/api/mistral-conversations.ts +++ b/packages/ai/src/api/mistral-conversations.ts @@ -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}` }; } diff --git a/packages/ai/test/mistral-raw-stop-reason.test.ts b/packages/ai/test/mistral-raw-stop-reason.test.ts index a24b33741..ee7881d7d 100644 --- a/packages/ai/test/mistral-raw-stop-reason.test.ts +++ b/packages/ai/test/mistral-raw-stop-reason.test.ts @@ -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); }); });