refactor(ai): migrate mistral provider to conversations sdk
This commit is contained in:
@@ -33,24 +33,6 @@ import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copi
|
||||
import { buildBaseOptions, clampReasoning } from "./simple-options.js";
|
||||
import { transformMessages } from "./transform-messages.js";
|
||||
|
||||
/**
|
||||
* Normalize tool call ID for Mistral.
|
||||
* Mistral requires tool IDs to be exactly 9 alphanumeric characters (a-z, A-Z, 0-9).
|
||||
*/
|
||||
function normalizeMistralToolId(id: string): string {
|
||||
// Remove non-alphanumeric characters
|
||||
let normalized = id.replace(/[^a-zA-Z0-9]/g, "");
|
||||
// Mistral requires exactly 9 characters
|
||||
if (normalized.length < 9) {
|
||||
// Pad with deterministic characters based on original ID to ensure matching
|
||||
const padding = "ABCDEFGHI";
|
||||
normalized = normalized + padding.slice(0, 9 - normalized.length);
|
||||
} else if (normalized.length > 9) {
|
||||
normalized = normalized.slice(0, 9);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if conversation messages contain tool calls or tool results.
|
||||
* This is needed because Anthropic (via proxy) requires the tools param
|
||||
@@ -296,7 +278,6 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
|
||||
}
|
||||
|
||||
finishCurrentBlock(currentBlock);
|
||||
|
||||
if (options?.signal?.aborted) {
|
||||
throw new Error("Request was aborted");
|
||||
}
|
||||
@@ -498,8 +479,6 @@ export function convertMessages(
|
||||
const params: ChatCompletionMessageParam[] = [];
|
||||
|
||||
const normalizeToolCallId = (id: string): string => {
|
||||
if (compat.requiresMistralToolIds) return normalizeMistralToolId(id);
|
||||
|
||||
// Handle pipe-separated IDs from OpenAI Responses API
|
||||
// Format: {call_id}|{id} where {id} can be 400+ chars with special chars (+, /, =)
|
||||
// These come from providers like github-copilot, openai-codex, opencode
|
||||
@@ -526,7 +505,7 @@ export function convertMessages(
|
||||
|
||||
for (let i = 0; i < transformedMessages.length; i++) {
|
||||
const msg = transformedMessages[i];
|
||||
// Some providers (e.g. Mistral/Devstral) don't allow user messages directly after tool results
|
||||
// Some providers don't allow user messages directly after tool results
|
||||
// Insert a synthetic assistant message to bridge the gap
|
||||
if (compat.requiresAssistantAfterToolResult && lastRole === "toolResult" && msg.role === "user") {
|
||||
params.push({
|
||||
@@ -567,7 +546,7 @@ export function convertMessages(
|
||||
});
|
||||
}
|
||||
} else if (msg.role === "assistant") {
|
||||
// Some providers (e.g. Mistral) don't accept null content, use empty string instead
|
||||
// Some providers don't accept null content, use empty string instead
|
||||
const assistantMsg: ChatCompletionAssistantMessageParam = {
|
||||
role: "assistant",
|
||||
content: compat.requiresAssistantAfterToolResult ? "" : null,
|
||||
@@ -636,7 +615,7 @@ export function convertMessages(
|
||||
}
|
||||
}
|
||||
// Skip assistant messages that have no content and no tool calls.
|
||||
// Mistral explicitly requires "either content or tool_calls, but not none".
|
||||
// Some providers require "either content or tool_calls, but not none".
|
||||
// Other providers also don't accept empty assistant messages.
|
||||
// This handles aborted assistant responses that got no content.
|
||||
const content = assistantMsg.content;
|
||||
@@ -664,7 +643,7 @@ export function convertMessages(
|
||||
|
||||
// Always send tool result with text (or placeholder if only images)
|
||||
const hasText = textResult.length > 0;
|
||||
// Some providers (e.g. Mistral) require the 'name' field in tool results
|
||||
// Some providers require the 'name' field in tool results
|
||||
const toolResultMsg: ChatCompletionToolMessageParam = {
|
||||
role: "tool",
|
||||
content: sanitizeSurrogates(hasText ? textResult : "(see attached image)"),
|
||||
@@ -773,21 +752,17 @@ function detectCompat(model: Model<"openai-completions">): Required<OpenAIComple
|
||||
baseUrl.includes("cerebras.ai") ||
|
||||
provider === "xai" ||
|
||||
baseUrl.includes("api.x.ai") ||
|
||||
provider === "mistral" ||
|
||||
baseUrl.includes("mistral.ai") ||
|
||||
baseUrl.includes("chutes.ai") ||
|
||||
baseUrl.includes("deepseek.com") ||
|
||||
isZai ||
|
||||
provider === "opencode" ||
|
||||
baseUrl.includes("opencode.ai");
|
||||
|
||||
const useMaxTokens = provider === "mistral" || baseUrl.includes("mistral.ai") || baseUrl.includes("chutes.ai");
|
||||
const useMaxTokens = baseUrl.includes("chutes.ai");
|
||||
|
||||
const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
|
||||
const isGroq = provider === "groq" || baseUrl.includes("groq.com");
|
||||
|
||||
const isMistral = provider === "mistral" || baseUrl.includes("mistral.ai");
|
||||
|
||||
const reasoningEffortMap =
|
||||
isGroq && model.id === "qwen/qwen3-32b"
|
||||
? {
|
||||
@@ -798,7 +773,6 @@ function detectCompat(model: Model<"openai-completions">): Required<OpenAIComple
|
||||
xhigh: "default",
|
||||
}
|
||||
: {};
|
||||
|
||||
return {
|
||||
supportsStore: !isNonStandard,
|
||||
supportsDeveloperRole: !isNonStandard,
|
||||
@@ -806,10 +780,9 @@ function detectCompat(model: Model<"openai-completions">): Required<OpenAIComple
|
||||
reasoningEffortMap,
|
||||
supportsUsageInStreaming: true,
|
||||
maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
|
||||
requiresToolResultName: isMistral,
|
||||
requiresAssistantAfterToolResult: false, // Mistral no longer requires this as of Dec 2024
|
||||
requiresThinkingAsText: isMistral,
|
||||
requiresMistralToolIds: isMistral,
|
||||
requiresToolResultName: false,
|
||||
requiresAssistantAfterToolResult: false,
|
||||
requiresThinkingAsText: false,
|
||||
thinkingFormat: isZai ? "zai" : "openai",
|
||||
openRouterRouting: {},
|
||||
vercelGatewayRouting: {},
|
||||
@@ -836,7 +809,6 @@ function getCompat(model: Model<"openai-completions">): Required<OpenAICompletio
|
||||
requiresAssistantAfterToolResult:
|
||||
model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult,
|
||||
requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
|
||||
requiresMistralToolIds: model.compat.requiresMistralToolIds ?? detected.requiresMistralToolIds,
|
||||
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
|
||||
openRouterRouting: model.compat.openRouterRouting ?? {},
|
||||
vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,
|
||||
|
||||
Reference in New Issue
Block a user