mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
fix(ai): use reasoning_effort for Mistral Small 4 closes #3338
This commit is contained in:
Generated
+6
-5
@@ -1781,13 +1781,14 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@mistralai/mistralai": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.14.1.tgz",
|
||||
"integrity": "sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.0.tgz",
|
||||
"integrity": "sha512-JQUGIXjFWnw/J9LpTSf/ZXwVW3Sh8FBAcfTo5QvAHqkl4CfSiIwnjRJhMoAFcP6ncCe84YPU1ncDGX+p3OXnfg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
"zod-to-json-schema": "^3.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas": {
|
||||
@@ -8576,7 +8577,7 @@
|
||||
"@anthropic-ai/sdk": "^0.90.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.1030.0",
|
||||
"@google/genai": "^1.40.0",
|
||||
"@mistralai/mistralai": "1.14.1",
|
||||
"@mistralai/mistralai": "^2.2.0",
|
||||
"@sinclair/typebox": "^0.34.41",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed Anthropic and Bedrock adaptive-thinking payload tests to expect the default `display: "summarized"` field when reasoning is enabled.
|
||||
- Fixed Mistral Small 4 reasoning requests to use `reasoning_effort` instead of `prompt_mode`, restoring default thinking support for `mistral-small-2603` and `mistral-small-latest` ([#3338](https://github.com/badlogic/pi-mono/issues/3338))
|
||||
- Fixed `qwen-chat-template` OpenAI-compatible requests to set `chat_template_kwargs.preserve_thinking: true`, preserving prior Qwen thinking across turns so multi-turn tool calls keep their arguments instead of degrading to empty `{}` payloads ([#3325](https://github.com/badlogic/pi-mono/issues/3325))
|
||||
|
||||
## [0.67.6] - 2026-04-16
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
"@anthropic-ai/sdk": "^0.90.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.1030.0",
|
||||
"@google/genai": "^1.40.0",
|
||||
"@mistralai/mistralai": "1.14.1",
|
||||
"@mistralai/mistralai": "^2.2.0",
|
||||
"@sinclair/typebox": "^0.34.41",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Mistral } from "@mistralai/mistralai";
|
||||
import type { RequestOptions } from "@mistralai/mistralai/lib/sdks.js";
|
||||
import type {
|
||||
ChatCompletionStreamRequest,
|
||||
ChatCompletionStreamRequestMessages,
|
||||
ChatCompletionStreamRequestMessage,
|
||||
CompletionEvent,
|
||||
ContentChunk,
|
||||
FunctionTool,
|
||||
} from "@mistralai/mistralai/models/components/index.js";
|
||||
} from "@mistralai/mistralai/models/components";
|
||||
import { getEnvApiKey } from "../env-api-keys.js";
|
||||
import { calculateCost } from "../models.js";
|
||||
import type {
|
||||
@@ -36,9 +35,12 @@ const MAX_MISTRAL_ERROR_BODY_CHARS = 4000;
|
||||
/**
|
||||
* Provider-specific options for the Mistral API.
|
||||
*/
|
||||
type MistralReasoningEffort = "none" | "high";
|
||||
|
||||
export interface MistralOptions extends StreamOptions {
|
||||
toolChoice?: "auto" | "none" | "any" | "required" | { type: "function"; function: { name: string } };
|
||||
promptMode?: "reasoning";
|
||||
reasoningEffort?: MistralReasoningEffort;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,10 +120,12 @@ export const streamSimpleMistral: StreamFunction<"mistral-conversations", Simple
|
||||
|
||||
const base = buildBaseOptions(model, options, apiKey);
|
||||
const reasoning = clampReasoning(options?.reasoning);
|
||||
const shouldUseReasoning = model.reasoning && reasoning !== undefined;
|
||||
|
||||
return streamMistral(model, context, {
|
||||
...base,
|
||||
promptMode: model.reasoning && reasoning ? "reasoning" : undefined,
|
||||
promptMode: shouldUseReasoning && usesPromptModeReasoning(model) ? "reasoning" : undefined,
|
||||
reasoningEffort: shouldUseReasoning && usesReasoningEffort(model) ? mapReasoningEffort(reasoning) : undefined,
|
||||
} satisfies MistralOptions);
|
||||
};
|
||||
|
||||
@@ -205,10 +209,15 @@ function safeJsonStringify(value: unknown): string {
|
||||
}
|
||||
}
|
||||
|
||||
function buildRequestOptions(model: Model<"mistral-conversations">, options?: MistralOptions): RequestOptions {
|
||||
const requestOptions: RequestOptions = {};
|
||||
function buildRequestOptions(model: Model<"mistral-conversations">, options?: MistralOptions) {
|
||||
const requestOptions: {
|
||||
signal?: AbortSignal;
|
||||
retries: { strategy: "none" };
|
||||
headers?: Record<string, string>;
|
||||
} = {
|
||||
retries: { strategy: "none" },
|
||||
};
|
||||
if (options?.signal) requestOptions.signal = options.signal;
|
||||
requestOptions.retries = { strategy: "none" };
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (model.headers) Object.assign(headers, model.headers);
|
||||
@@ -243,7 +252,8 @@ function buildChatPayload(
|
||||
if (options?.temperature !== undefined) payload.temperature = options.temperature;
|
||||
if (options?.maxTokens !== undefined) payload.maxTokens = options.maxTokens;
|
||||
if (options?.toolChoice) payload.toolChoice = mapToolChoice(options.toolChoice);
|
||||
if (options?.promptMode) payload.promptMode = options.promptMode as any;
|
||||
if (options?.promptMode) payload.promptMode = options.promptMode;
|
||||
if (options?.reasoningEffort) payload.reasoningEffort = options.reasoningEffort;
|
||||
|
||||
if (context.systemPrompt) {
|
||||
payload.messages.unshift({
|
||||
@@ -452,8 +462,8 @@ function toFunctionTools(tools: Tool[]): Array<FunctionTool & { type: "function"
|
||||
}));
|
||||
}
|
||||
|
||||
function toChatMessages(messages: Message[], supportsImages: boolean): ChatCompletionStreamRequestMessages[] {
|
||||
const result: ChatCompletionStreamRequestMessages[] = [];
|
||||
function toChatMessages(messages: Message[], supportsImages: boolean): ChatCompletionStreamRequestMessage[] {
|
||||
const result: ChatCompletionStreamRequestMessage[] = [];
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "user") {
|
||||
@@ -505,7 +515,7 @@ function toChatMessages(messages: Message[], supportsImages: boolean): ChatCompl
|
||||
});
|
||||
}
|
||||
|
||||
const assistantMessage: ChatCompletionStreamRequestMessages = { role: "assistant" };
|
||||
const assistantMessage: ChatCompletionStreamRequestMessage = { role: "assistant" };
|
||||
if (contentParts.length > 0) assistantMessage.content = contentParts;
|
||||
if (toolCalls.length > 0) assistantMessage.toolCalls = toolCalls;
|
||||
if (contentParts.length > 0 || toolCalls.length > 0) result.push(assistantMessage);
|
||||
@@ -560,6 +570,18 @@ function buildToolResultText(text: string, hasImages: boolean, supportsImages: b
|
||||
return isError ? "[tool error] (no tool output)" : "(no tool output)";
|
||||
}
|
||||
|
||||
function usesReasoningEffort(model: Model<"mistral-conversations">): boolean {
|
||||
return model.id === "mistral-small-2603" || model.id === "mistral-small-latest";
|
||||
}
|
||||
|
||||
function usesPromptModeReasoning(model: Model<"mistral-conversations">): boolean {
|
||||
return model.reasoning && !usesReasoningEffort(model);
|
||||
}
|
||||
|
||||
function mapReasoningEffort(_level: Exclude<SimpleStreamOptions["reasoning"], undefined>): MistralReasoningEffort {
|
||||
return "high";
|
||||
}
|
||||
|
||||
function mapToolChoice(
|
||||
choice: MistralOptions["toolChoice"],
|
||||
): "auto" | "none" | "any" | "required" | { type: "function"; function: { name: string } } | undefined {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getModel } from "../src/models.js";
|
||||
import { streamSimple } from "../src/stream.js";
|
||||
import type { Context, Model, SimpleStreamOptions } from "../src/types.js";
|
||||
|
||||
interface MistralPayload {
|
||||
promptMode?: "reasoning";
|
||||
reasoningEffort?: "none" | "high";
|
||||
}
|
||||
|
||||
function makeContext(): Context {
|
||||
return {
|
||||
messages: [{ role: "user", content: "Hello", timestamp: Date.now() }],
|
||||
};
|
||||
}
|
||||
|
||||
async function capturePayload(
|
||||
model: Model<"mistral-conversations">,
|
||||
options?: SimpleStreamOptions,
|
||||
): Promise<MistralPayload> {
|
||||
let capturedPayload: MistralPayload | undefined;
|
||||
const payloadCaptureModel: Model<"mistral-conversations"> = {
|
||||
...model,
|
||||
baseUrl: "http://127.0.0.1:9",
|
||||
};
|
||||
|
||||
const stream = streamSimple(payloadCaptureModel, makeContext(), {
|
||||
...options,
|
||||
apiKey: "fake-key",
|
||||
onPayload: (payload) => {
|
||||
capturedPayload = payload as MistralPayload;
|
||||
return payload;
|
||||
},
|
||||
});
|
||||
|
||||
await stream.result();
|
||||
|
||||
if (!capturedPayload) {
|
||||
throw new Error("Expected payload to be captured before request failure");
|
||||
}
|
||||
|
||||
return capturedPayload;
|
||||
}
|
||||
|
||||
describe("Mistral reasoning mode selection", () => {
|
||||
it("uses reasoning_effort for Mistral Small 4", async () => {
|
||||
const payload = await capturePayload(getModel("mistral", "mistral-small-2603"), { reasoning: "medium" });
|
||||
|
||||
expect(payload.reasoningEffort).toBe("high");
|
||||
expect(payload.promptMode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits reasoning controls for Mistral Small 4 when thinking is off", async () => {
|
||||
const payload = await capturePayload(getModel("mistral", "mistral-small-2603"));
|
||||
|
||||
expect(payload.reasoningEffort).toBeUndefined();
|
||||
expect(payload.promptMode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses prompt_mode for Magistral reasoning models", async () => {
|
||||
const payload = await capturePayload(getModel("mistral", "magistral-medium-latest"), { reasoning: "medium" });
|
||||
|
||||
expect(payload.promptMode).toBe("reasoning");
|
||||
expect(payload.reasoningEffort).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed `/scoped-models` Alt+Up/Down to stay a no-op in the implicit `all enabled` state instead of materializing a full explicit enabled-model list and marking the selector dirty ([#3331](https://github.com/badlogic/pi-mono/issues/3331))
|
||||
- Fixed Mistral Small 4 default thinking requests to use the model's supported reasoning control, avoiding `400` errors when starting sessions on `mistral-small-2603` and `mistral-small-latest` ([#3338](https://github.com/badlogic/pi-mono/issues/3338))
|
||||
- Fixed flaky git package update notifications by waiting for captured git command stdio to fully drain before comparing local and remote commit SHAs ([#3027](https://github.com/badlogic/pi-mono/issues/3027))
|
||||
- Fixed auto-retry transient error detection to treat `Network connection lost.` as retryable, so dropped provider connections retry instead of terminating the agent ([#3317](https://github.com/badlogic/pi-mono/issues/3317))
|
||||
- Fixed compact interactive extension startup summaries to disambiguate package extensions and repeated local `index.ts` entries by using package-aware labels and the minimal parent path needed to make local entries unique ([#3308](https://github.com/badlogic/pi-mono/issues/3308))
|
||||
|
||||
Reference in New Issue
Block a user