diff --git a/packages/ai/src/api/mistral-conversations.ts b/packages/ai/src/api/mistral-conversations.ts index 160094381..fbc51047a 100644 --- a/packages/ai/src/api/mistral-conversations.ts +++ b/packages/ai/src/api/mistral-conversations.ts @@ -89,7 +89,7 @@ export const stream: StreamFunction<"mistral-conversations", MistralOptions> = ( throw new Error("Mistral stream ended without a finish reason"); } if (output.stopReason === "aborted" || output.stopReason === "error") { - throw new Error("An unknown error occurred"); + throw new Error(output.errorMessage || "An unknown error occurred"); } stream.push({ type: "done", reason: output.stopReason, message: output }); @@ -353,7 +353,12 @@ async function consumeChatStream( if (!choice) continue; if (choice.finishReason) { - output.stopReason = mapChatStopReason(choice.finishReason); + output.rawStopReason = choice.finishReason; + const stopReasonResult = mapChatStopReason(choice.finishReason); + output.stopReason = stopReasonResult.stopReason; + if (stopReasonResult.errorMessage) { + output.errorMessage = stopReasonResult.errorMessage; + } } const delta = choice.delta; @@ -654,19 +659,19 @@ function mapToolChoice( }; } -function mapChatStopReason(reason: string | null): StopReason { - if (reason === null) return "stop"; +function mapChatStopReason(reason: string | null): { stopReason: StopReason; errorMessage?: string } { + if (reason === null) return { stopReason: "stop" }; switch (reason) { case "stop": - return "stop"; + return { stopReason: "stop" }; case "length": case "model_length": - return "length"; + return { stopReason: "length" }; case "tool_calls": - return "toolUse"; + return { stopReason: "toolUse" }; case "error": - return "error"; + return { stopReason: "error", errorMessage: "Mistral stopped with: error" }; default: - return "stop"; + return { stopReason: "error", errorMessage: `Mistral 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 new file mode 100644 index 000000000..0e7ae5e52 --- /dev/null +++ b/packages/ai/test/mistral-raw-stop-reason.test.ts @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mistralMock = vi.hoisted(() => ({ + finishReason: "stop" as string, +})); + +vi.mock("@mistralai/mistralai", () => { + class HTTPClient {} + + class Mistral { + chat = { + stream: async function* () { + yield { + data: { + id: "mistral-response-id", + choices: [ + { + finishReason: mistralMock.finishReason, + delta: {}, + }, + ], + usage: { + promptTokens: 1, + completionTokens: 0, + totalTokens: 1, + }, + }, + }; + }, + }; + } + + return { HTTPClient, Mistral }; +}); + +import { stream as streamMistral } from "../src/api/mistral-conversations.ts"; +import { getModel } from "../src/compat.ts"; +import type { Context } from "../src/types.ts"; + +const model = getModel("mistral", "devstral-medium-latest"); +const context: Context = { + messages: [{ role: "user", content: "hello", timestamp: Date.now() }], +}; + +describe("Mistral raw stop reasons", () => { + beforeEach(() => { + mistralMock.finishReason = "stop"; + }); + + it("preserves raw Mistral finish reasons for successful stops", async () => { + const message = await streamMistral(model, context, { apiKey: "test" }).result(); + + expect(message.stopReason).toBe("stop"); + expect(message.rawStopReason).toBe("stop"); + expect(message.errorMessage).toBeUndefined(); + }); + + it("preserves raw Mistral finish reasons for provider error stops", async () => { + mistralMock.finishReason = "error"; + + const message = await streamMistral(model, context, { apiKey: "test" }).result(); + + expect(message.stopReason).toBe("error"); + expect(message.rawStopReason).toBe("error"); + expect(message.errorMessage).toBe("Mistral stopped with: error"); + }); + + it("treats unknown Mistral finish reasons as provider error stops", async () => { + mistralMock.finishReason = "unmapped_error"; + + const message = await streamMistral(model, context, { apiKey: "test" }).result(); + + expect(message.stopReason).toBe("error"); + expect(message.rawStopReason).toBe("unmapped_error"); + expect(message.errorMessage).toBe("Mistral stopped with: unmapped_error"); + }); +});