mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(mistral): report provider terminal failures instead of silent success (#129256)
This commit is contained in:
committed by
GitHub
parent
2e54fe7116
commit
ebfd80ff91
@@ -169,7 +169,7 @@ describe("Mistral bounded-stream-read direct (synthetic ReadableStream)", () =>
|
||||
});
|
||||
|
||||
type MistralTerminalFixture = {
|
||||
finishReason: "stop" | "length" | "error" | "tool_calls" | null;
|
||||
finishReason: string | null;
|
||||
done: boolean;
|
||||
abort?: boolean;
|
||||
toolArguments?: string[];
|
||||
@@ -269,6 +269,8 @@ describe("Mistral terminal ownership through the installed SDK and real HTTP/SSE
|
||||
{ name: "EOF without a provider terminal", finishReason: null, done: false },
|
||||
{ name: "DONE without a provider terminal", finishReason: null, done: true },
|
||||
{ name: "a provider error terminal", finishReason: "error", done: true },
|
||||
{ name: "a filtered provider terminal", finishReason: "content_filter", done: true },
|
||||
{ name: "an unknown provider terminal", finishReason: "provider_guardrail", done: true },
|
||||
{ name: "malformed arguments on a tool terminal", finishReason: "tool_calls", done: true },
|
||||
] as const)("rejects $name without executable calls", async (fixture) => {
|
||||
const { result, events } = await streamMistralTerminalFixture({
|
||||
@@ -279,24 +281,31 @@ describe("Mistral terminal ownership through the installed SDK and real HTTP/SSE
|
||||
expect(result.stopReason).toBe("error");
|
||||
if (fixture.finishReason === null) {
|
||||
expect(result.errorMessage).toBe("Mistral stream ended without a terminal finish reason");
|
||||
} else if (fixture.finishReason === "tool_calls") {
|
||||
expect(result.errorMessage).toContain("invalid JSON arguments");
|
||||
} else {
|
||||
expect(result.errorMessage).toBeTruthy();
|
||||
expect(result.errorMessage).toBe(`Provider finish_reason: ${fixture.finishReason}`);
|
||||
}
|
||||
expect(events).toContain("error");
|
||||
expect(events).not.toContain("toolcall_end");
|
||||
expect(result.content).not.toContainEqual(expect.objectContaining({ type: "toolCall" }));
|
||||
expect(result.content).toContainEqual({ type: "text", text: "Safe partial answer" });
|
||||
});
|
||||
|
||||
it.each(["length", "stop"] as const)(
|
||||
"preserves a %s terminal and visible text without finalizing partial tools",
|
||||
async (finishReason) => {
|
||||
it.each([
|
||||
{ finishReason: "length", stopReason: "length" },
|
||||
{ finishReason: "model_length", stopReason: "length" },
|
||||
{ finishReason: "stop", stopReason: "stop" },
|
||||
] as const)(
|
||||
"preserves a $finishReason terminal and visible text without finalizing partial tools",
|
||||
async ({ finishReason, stopReason }) => {
|
||||
const { result, events } = await streamMistralTerminalFixture({
|
||||
finishReason,
|
||||
done: true,
|
||||
toolArguments: ['{"action":"delete_all"'],
|
||||
text: "Safe partial answer",
|
||||
});
|
||||
expect(result.stopReason).toBe(finishReason);
|
||||
expect(result.stopReason).toBe(stopReason);
|
||||
expect(result.content).toEqual([{ type: "text", text: "Safe partial answer" }]);
|
||||
expect(events).not.toContain("toolcall_end");
|
||||
expect(events).toContain("done");
|
||||
|
||||
@@ -23,7 +23,6 @@ import type {
|
||||
Message,
|
||||
Model,
|
||||
SimpleStreamOptions,
|
||||
StopReason,
|
||||
StreamFunction,
|
||||
StreamOptions,
|
||||
TextContent,
|
||||
@@ -43,6 +42,7 @@ import { projectProviderError } from "../utils/provider-error.js";
|
||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||
import { createSseByteGuard } from "../utils/streaming-byte-guard.js";
|
||||
import { stripSystemPromptCacheBoundary } from "../utils/system-prompt-cache-boundary.js";
|
||||
import { mapOpenAIStopReason } from "./openai-stop-reason.js";
|
||||
import { buildBaseOptions, clampMaxTokensToModel } from "./simple-options.js";
|
||||
import {
|
||||
describeToolResultMediaPlaceholder,
|
||||
@@ -206,7 +206,7 @@ export const streamMistral: StreamFunction<"mistral-conversations", MistralOptio
|
||||
}
|
||||
|
||||
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 });
|
||||
@@ -619,7 +619,13 @@ async function consumeChatStream(
|
||||
|
||||
if (choice.finishReason) {
|
||||
terminalFinishReason = choice.finishReason;
|
||||
output.stopReason = mapChatStopReason(choice.finishReason);
|
||||
const { stopReason, errorMessage } = mapOpenAIStopReason(
|
||||
choice.finishReason === "model_length" ? "length" : choice.finishReason,
|
||||
);
|
||||
output.stopReason = stopReason;
|
||||
if (errorMessage) {
|
||||
output.errorMessage = errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
const delta = choice.delta;
|
||||
@@ -1018,22 +1024,4 @@ function mapToolChoice(
|
||||
};
|
||||
}
|
||||
|
||||
function mapChatStopReason(reason: string | null): StopReason {
|
||||
if (reason === null) {
|
||||
return "stop";
|
||||
}
|
||||
switch (reason) {
|
||||
case "stop":
|
||||
return "stop";
|
||||
case "length":
|
||||
case "model_length":
|
||||
return "length";
|
||||
case "tool_calls":
|
||||
return "toolUse";
|
||||
case "error":
|
||||
return "error";
|
||||
default:
|
||||
return "stop";
|
||||
}
|
||||
}
|
||||
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|
||||
|
||||
Reference in New Issue
Block a user