Add Mistral as AI provider

- Add Mistral to KnownProvider type and model generation
- Implement Mistral-specific compat handling in openai-completions:
  - requiresToolResultName: tool results need name field
  - requiresAssistantAfterToolResult: synthetic assistant message between tool/user
  - requiresThinkingAsText: thinking blocks as <thinking> text
  - requiresMistralToolIds: tool IDs must be exactly 9 alphanumeric chars
- Add MISTRAL_API_KEY environment variable support
- Add Mistral tests across all test files
- Update documentation (README, CHANGELOG) for both ai and coding-agent packages
- Remove client IDs from gemini.md, reference upstream source instead

Closes #165
This commit is contained in:
Mario Zechner 2025-12-10 20:36:19 +01:00
parent a248e2547a
commit 99b4b1aca0
31 changed files with 1856 additions and 282 deletions

View file

@ -1989,6 +1989,416 @@ export const MODELS = {
contextWindow: 204800,
maxTokens: 131072,
} satisfies Model<"anthropic-messages">,
"glm-4.6v": {
id: "glm-4.6v",
name: "GLM-4.6V",
api: "anthropic-messages",
provider: "zai",
baseUrl: "https://api.z.ai/api/anthropic",
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.3,
output: 0.9,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 32768,
} satisfies Model<"anthropic-messages">,
},
mistral: {
"devstral-medium-2507": {
id: "devstral-medium-2507",
name: "Devstral Medium",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.4,
output: 2,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"mistral-large-2512": {
id: "mistral-large-2512",
name: "Mistral Large 3",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.5,
output: 1.5,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"open-mixtral-8x22b": {
id: "open-mixtral-8x22b",
name: "Mixtral 8x22B",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text"],
cost: {
input: 2,
output: 6,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 64000,
maxTokens: 64000,
} satisfies Model<"openai-completions">,
"ministral-8b-latest": {
id: "ministral-8b-latest",
name: "Ministral 8B",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.1,
output: 0.1,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"pixtral-large-latest": {
id: "pixtral-large-latest",
name: "Pixtral Large",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 2,
output: 6,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"ministral-3b-latest": {
id: "ministral-3b-latest",
name: "Ministral 3B",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.04,
output: 0.04,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"pixtral-12b": {
id: "pixtral-12b",
name: "Pixtral 12B",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.15,
output: 0.15,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"mistral-medium-2505": {
id: "mistral-medium-2505",
name: "Mistral Medium 3",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.4,
output: 2,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"labs-devstral-small-2512": {
id: "labs-devstral-small-2512",
name: "Devstral Small 2",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.1,
output: 0.3,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 256000,
} satisfies Model<"openai-completions">,
"devstral-medium-latest": {
id: "devstral-medium-latest",
name: "Devstral 2",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.4,
output: 2,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"devstral-small-2505": {
id: "devstral-small-2505",
name: "Devstral Small 2505",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.1,
output: 0.3,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"mistral-medium-2508": {
id: "mistral-medium-2508",
name: "Mistral Medium 3.1",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.4,
output: 2,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"mistral-small-latest": {
id: "mistral-small-latest",
name: "Mistral Small",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.1,
output: 0.3,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"magistral-small": {
id: "magistral-small",
name: "Magistral Small",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0.5,
output: 1.5,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"devstral-small-2507": {
id: "devstral-small-2507",
name: "Devstral Small",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.1,
output: 0.3,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"codestral-latest": {
id: "codestral-latest",
name: "Codestral",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.3,
output: 0.9,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"open-mixtral-8x7b": {
id: "open-mixtral-8x7b",
name: "Mixtral 8x7B",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.7,
output: 0.7,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 32000,
maxTokens: 32000,
} satisfies Model<"openai-completions">,
"mistral-nemo": {
id: "mistral-nemo",
name: "Mistral Nemo",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.15,
output: 0.15,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"open-mistral-7b": {
id: "open-mistral-7b",
name: "Mistral 7B",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.25,
output: 0.25,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 8000,
maxTokens: 8000,
} satisfies Model<"openai-completions">,
"mistral-large-latest": {
id: "mistral-large-latest",
name: "Mistral Large",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.5,
output: 1.5,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"mistral-medium-latest": {
id: "mistral-medium-latest",
name: "Mistral Medium",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.4,
output: 2,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"mistral-large-2411": {
id: "mistral-large-2411",
name: "Mistral Large 2.1",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text"],
cost: {
input: 2,
output: 6,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"magistral-medium-latest": {
id: "magistral-medium-latest",
name: "Magistral Medium",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: true,
input: ["text"],
cost: {
input: 2,
output: 5,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
},
openrouter: {
"mistralai/devstral-2512:free": {
@ -4448,13 +4858,13 @@ export const MODELS = {
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.136,
output: 0.6799999999999999,
input: 0.15,
output: 0.6,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 1048576,
maxTokens: 8192,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-4-scout": {
id: "meta-llama/llama-4-scout",
@ -5068,23 +5478,6 @@ export const MODELS = {
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"openai-completions">,
"mistralai/ministral-3b": {
id: "mistralai/ministral-3b",
name: "Mistral: Ministral 3B",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.04,
output: 0.04,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/ministral-8b": {
id: "mistralai/ministral-8b",
name: "Mistral: Ministral 8B",
@ -5102,6 +5495,23 @@ export const MODELS = {
contextWindow: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/ministral-3b": {
id: "mistralai/ministral-3b",
name: "Mistral: Ministral 3B",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.04,
output: 0.04,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"nvidia/llama-3.1-nemotron-70b-instruct": {
id: "nvidia/llama-3.1-nemotron-70b-instruct",
name: "NVIDIA: Llama 3.1 Nemotron 70B Instruct",
@ -5272,6 +5682,23 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-8b-instruct": {
id: "meta-llama/llama-3.1-8b-instruct",
name: "Meta: Llama 3.1 8B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.02,
output: 0.03,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-405b-instruct": {
id: "meta-llama/llama-3.1-405b-instruct",
name: "Meta: Llama 3.1 405B Instruct",
@ -5306,23 +5733,6 @@ export const MODELS = {
contextWindow: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-8b-instruct": {
id: "meta-llama/llama-3.1-8b-instruct",
name: "Meta: Llama 3.1 8B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.02,
output: 0.03,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"mistralai/mistral-nemo": {
id: "mistralai/mistral-nemo",
name: "Mistral: Mistral Nemo",
@ -5459,6 +5869,23 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4o-2024-05-13": {
id: "openai/gpt-4o-2024-05-13",
name: "OpenAI: GPT-4o (2024-05-13)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 5,
output: 15,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4o": {
id: "openai/gpt-4o",
name: "OpenAI: GPT-4o",
@ -5493,22 +5920,22 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 64000,
} satisfies Model<"openai-completions">,
"openai/gpt-4o-2024-05-13": {
id: "openai/gpt-4o-2024-05-13",
name: "OpenAI: GPT-4o (2024-05-13)",
"meta-llama/llama-3-70b-instruct": {
id: "meta-llama/llama-3-70b-instruct",
name: "Meta: Llama 3 70B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text", "image"],
input: ["text"],
cost: {
input: 5,
output: 15,
input: 0.3,
output: 0.39999999999999997,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
contextWindow: 8192,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3-8b-instruct": {
id: "meta-llama/llama-3-8b-instruct",
@ -5527,23 +5954,6 @@ export const MODELS = {
contextWindow: 8192,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3-70b-instruct": {
id: "meta-llama/llama-3-70b-instruct",
name: "Meta: Llama 3 70B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.3,
output: 0.39999999999999997,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 8192,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"mistralai/mixtral-8x22b-instruct": {
id: "mistralai/mixtral-8x22b-instruct",
name: "Mistral: Mixtral 8x22B Instruct",
@ -5629,23 +6039,6 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4-turbo-preview": {
id: "openai/gpt-4-turbo-preview",
name: "OpenAI: GPT-4 Turbo Preview",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 10,
output: 30,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-3.5-turbo-0613": {
id: "openai/gpt-3.5-turbo-0613",
name: "OpenAI: GPT-3.5 Turbo (older v0613)",
@ -5663,6 +6056,23 @@ export const MODELS = {
contextWindow: 4095,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4-turbo-preview": {
id: "openai/gpt-4-turbo-preview",
name: "OpenAI: GPT-4 Turbo Preview",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 10,
output: 30,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/mistral-tiny": {
id: "mistralai/mistral-tiny",
name: "Mistral Tiny",
@ -5731,6 +6141,23 @@ export const MODELS = {
contextWindow: 16385,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4-0314": {
id: "openai/gpt-4-0314",
name: "OpenAI: GPT-4 (older v0314)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 30,
output: 60,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 8191,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4": {
id: "openai/gpt-4",
name: "OpenAI: GPT-4",
@ -5765,23 +6192,6 @@ export const MODELS = {
contextWindow: 16385,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4-0314": {
id: "openai/gpt-4-0314",
name: "OpenAI: GPT-4 (older v0314)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 30,
output: 60,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 8191,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openrouter/auto": {
id: "openrouter/auto",
name: "OpenRouter: Auto Router",

View file

@ -6,6 +6,7 @@ import type {
ChatCompletionContentPartImage,
ChatCompletionContentPartText,
ChatCompletionMessageParam,
ChatCompletionToolMessageParam,
} from "openai/resources/chat/completions.js";
import { calculateCost } from "../models.js";
import type {
@ -27,6 +28,25 @@ import { parseStreamingJson } from "../utils/json-parse.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import { transformMessages } from "./transorm-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, isMistral: boolean): string {
if (!isMistral) return id;
// 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
@ -346,7 +366,18 @@ function convertMessages(
params.push({ role: role, content: sanitizeSurrogates(context.systemPrompt) });
}
let lastRole: string | null = null;
for (const msg of transformedMessages) {
// Some providers (e.g. Mistral) 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({
role: "assistant",
content: "I have processed the tool results.",
});
}
if (msg.role === "user") {
if (typeof msg.content === "string") {
params.push({
@ -379,9 +410,10 @@ function convertMessages(
});
}
} else if (msg.role === "assistant") {
// Some providers (e.g. Mistral) don't accept null content, use empty string instead
const assistantMsg: ChatCompletionAssistantMessageParam = {
role: "assistant",
content: null,
content: compat.requiresAssistantAfterToolResult ? "" : null,
};
const textBlocks = msg.content.filter((b) => b.type === "text") as TextContent[];
@ -391,20 +423,31 @@ function convertMessages(
});
}
// Handle thinking blocks for llama.cpp server + gpt-oss
// Handle thinking blocks
const thinkingBlocks = msg.content.filter((b) => b.type === "thinking") as ThinkingContent[];
if (thinkingBlocks.length > 0) {
// Use the signature from the first thinking block if available
const signature = thinkingBlocks[0].thinkingSignature;
if (signature && signature.length > 0) {
(assistantMsg as any)[signature] = thinkingBlocks.map((b) => b.thinking).join("\n");
if (compat.requiresThinkingAsText) {
// Convert thinking blocks to text with <thinking> delimiters
const thinkingText = thinkingBlocks.map((b) => `<thinking>\n${b.thinking}\n</thinking>`).join("\n");
const textContent = assistantMsg.content as Array<{ type: "text"; text: string }> | null;
if (textContent) {
textContent.unshift({ type: "text", text: thinkingText });
} else {
assistantMsg.content = [{ type: "text", text: thinkingText }];
}
} else {
// Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss)
const signature = thinkingBlocks[0].thinkingSignature;
if (signature && signature.length > 0) {
(assistantMsg as any)[signature] = thinkingBlocks.map((b) => b.thinking).join("\n");
}
}
}
const toolCalls = msg.content.filter((b) => b.type === "toolCall") as ToolCall[];
if (toolCalls.length > 0) {
assistantMsg.tool_calls = toolCalls.map((tc) => ({
id: tc.id,
id: normalizeMistralToolId(tc.id, compat.requiresMistralToolIds),
type: "function" as const,
function: {
name: tc.name,
@ -426,11 +469,16 @@ function convertMessages(
// Always send tool result with text (or placeholder if only images)
const hasText = textResult.length > 0;
params.push({
// Some providers (e.g. Mistral) require the 'name' field in tool results
const toolResultMsg: ChatCompletionToolMessageParam = {
role: "tool",
content: sanitizeSurrogates(hasText ? textResult : "(see attached image)"),
tool_call_id: msg.toolCallId,
});
tool_call_id: normalizeMistralToolId(msg.toolCallId, compat.requiresMistralToolIds),
};
if (compat.requiresToolResultName && msg.toolName) {
(toolResultMsg as any).name = msg.toolName;
}
params.push(toolResultMsg);
// If there are images and model supports them, send a follow-up user message with images
if (hasImages && model.input.includes("image")) {
@ -462,6 +510,8 @@ function convertMessages(
});
}
}
lastRole = msg.role;
}
return params;
@ -512,11 +562,17 @@ function detectCompatFromUrl(baseUrl: string): Required<OpenAICompat> {
const isGrok = baseUrl.includes("api.x.ai");
const isMistral = baseUrl.includes("mistral.ai");
return {
supportsStore: !isNonStandard,
supportsDeveloperRole: !isNonStandard,
supportsReasoningEffort: !isGrok,
maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
requiresToolResultName: isMistral,
requiresAssistantAfterToolResult: isMistral,
requiresThinkingAsText: isMistral,
requiresMistralToolIds: isMistral,
};
}
@ -533,5 +589,10 @@ function getCompat(model: Model<"openai-completions">): Required<OpenAICompat> {
supportsDeveloperRole: model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole,
supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort,
maxTokensField: model.compat.maxTokensField ?? detected.maxTokensField,
requiresToolResultName: model.compat.requiresToolResultName ?? detected.requiresToolResultName,
requiresAssistantAfterToolResult:
model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult,
requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
requiresMistralToolIds: model.compat.requiresMistralToolIds ?? detected.requiresMistralToolIds,
};
}

View file

@ -39,6 +39,7 @@ export function getApiKey(provider: any): string | undefined {
xai: "XAI_API_KEY",
openrouter: "OPENROUTER_API_KEY",
zai: "ZAI_API_KEY",
mistral: "MISTRAL_API_KEY",
};
const envVar = envMap[provider];

View file

@ -26,7 +26,16 @@ const _exhaustive: _CheckExhaustive = true;
// Helper type to get options for a specific API
export type OptionsForApi<TApi extends Api> = ApiOptionsMap[TApi];
export type KnownProvider = "anthropic" | "google" | "openai" | "xai" | "groq" | "cerebras" | "openrouter" | "zai";
export type KnownProvider =
| "anthropic"
| "google"
| "openai"
| "xai"
| "groq"
| "cerebras"
| "openrouter"
| "zai"
| "mistral";
export type Provider = KnownProvider | string;
export type ReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
@ -165,6 +174,14 @@ export interface OpenAICompat {
supportsReasoningEffort?: boolean;
/** Which field to use for max tokens. Default: auto-detected from URL. */
maxTokensField?: "max_completion_tokens" | "max_tokens";
/** Whether tool results require the `name` field. Default: auto-detected from URL. */
requiresToolResultName?: boolean;
/** Whether a user message after tool results requires an assistant message in between. Default: auto-detected from URL. */
requiresAssistantAfterToolResult?: boolean;
/** Whether thinking blocks must be converted to text blocks with <thinking> delimiters. Default: auto-detected from URL. */
requiresThinkingAsText?: boolean;
/** Whether tool call IDs must be normalized to Mistral format (exactly 9 alphanumeric chars). Default: auto-detected from URL. */
requiresMistralToolIds?: boolean;
}
// Model interface for the unified model system

View file

@ -17,6 +17,7 @@ import type { AssistantMessage } from "../types.js";
* - llama.cpp: "the request exceeds the available context size, try increasing it"
* - LM Studio: "tokens to keep from the initial prompt is greater than the context length"
* - Cerebras: Returns "400 status code (no body)" - handled separately below
* - Mistral: Returns "400 status code (no body)" - handled separately below
* - z.ai: Does NOT error, accepts overflow silently - handled via usage.input > contextWindow
* - Ollama: Silently truncates input - not detectable via error message
*/
@ -52,6 +53,7 @@ const OVERFLOW_PATTERNS = [
* - xAI (Grok): "maximum prompt length is X but request contains Y"
* - Groq: "reduce the length of the messages"
* - Cerebras: 400/413 status code (no body)
* - Mistral: 400/413 status code (no body)
* - OpenRouter (all backends): "maximum context length is X tokens"
* - llama.cpp: "exceeds the available context size"
* - LM Studio: "greater than the context length"
@ -85,7 +87,7 @@ export function isContextOverflow(message: AssistantMessage, contextWindow?: num
return true;
}
// Cerebras returns 400/413 with no body - check for status code pattern
// Cerebras and Mistral return 400/413 with no body - check for status code pattern
if (/^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message.errorMessage)) {
return true;
}