chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:34:48 +08:00
commit 77bb5bf71f
3762 changed files with 353249 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# Mistral AI Integration
This integration allows your bot to choose from a curated list of [Mistral AI Models](https://docs.mistral.ai/getting-started/models) as the LLM of your choice for a node, workflow, or skill in your bot.
Usage is charged to the AI Spend of your workspace in Botpress Cloud at the [same pricing](https://mistral.ai/pricing#api-pricing) (at cost) as directly with Mistral.
+19
View File
@@ -0,0 +1,19 @@
<svg width="241" height="241" viewBox="0 0 191 135" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_134_208)">
<path d="M54.3221 0H27.1531V27.0892H54.3221V0Z" fill="#FFD800"/>
<path d="M162.984 0H135.815V27.0892H162.984V0Z" fill="#FFD800"/>
<path d="M81.4823 27.0913H27.1531V54.1805H81.4823V27.0913Z" fill="#FFAF00"/>
<path d="M162.99 27.0913H108.661V54.1805H162.99V27.0913Z" fill="#FFAF00"/>
<path d="M162.972 54.168H27.1531V81.2572H162.972V54.168Z" fill="#FF8205"/>
<path d="M54.3221 81.2593H27.1531V108.349H54.3221V81.2593Z" fill="#FA500F"/>
<path d="M108.661 81.2593H81.4917V108.349H108.661V81.2593Z" fill="#FA500F"/>
<path d="M162.984 81.2593H135.815V108.349H162.984V81.2593Z" fill="#FA500F"/>
<path d="M81.4879 108.339H-0.00146484V135.429H81.4879V108.339Z" fill="#E10500"/>
<path d="M190.159 108.339H108.661V135.429H190.159V108.339Z" fill="#E10500"/>
</g>
<defs>
<clipPath id="clip0_134_208">
<rect width="190.141" height="135" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1001 B

@@ -0,0 +1,30 @@
import { IntegrationDefinition, z } from '@botpress/sdk'
import { ModelId } from 'src/schemas'
import llm from './bp_modules/llm'
export default new IntegrationDefinition({
name: 'mistral-ai',
title: 'Mistral AI',
description: 'Access a curated list of Mistral AI models to set as your chosen LLM.',
version: '1.0.0',
readme: 'hub.md',
icon: 'icon.svg',
entities: {
modelRef: {
schema: z.object({
id: ModelId,
}),
},
},
secrets: {
MISTRAL_API_KEY: {
description: 'Mistral AI API key',
},
},
attributes: {
category: 'AI Models',
repo: 'botpress',
},
}).extend(llm, ({ entities }) => ({
entities: { modelRef: entities.modelRef },
}))
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@botpresshub/mistral-ai",
"scripts": {
"build": "bp add -y && bp build",
"check:type": "tsc --noEmit",
"check:bplint": "bp lint",
"test": "vitest --run"
},
"private": true,
"dependencies": {
"@botpress/client": "workspace:*",
"@botpress/common": "workspace:*",
"@botpress/sdk": "workspace:*",
"@mistralai/mistralai": "^1.11.0"
},
"devDependencies": {
"@botpress/cli": "workspace:*",
"@botpress/sdk": "workspace:*",
"@botpresshub/llm": "workspace:*"
},
"bpDependencies": {
"llm": "../../interfaces/llm"
}
}
@@ -0,0 +1,458 @@
import { InvalidPayloadError } from '@botpress/client'
import { llm } from '@botpress/common'
import { IntegrationLogger, z } from '@botpress/sdk'
import { Mistral } from '@mistralai/mistralai'
import type {
Messages,
ChatCompletionRequest,
ChatCompletionResponse,
Tool,
ToolChoice,
ToolChoiceEnum,
ContentChunk,
ToolCall,
FinishReason,
} from '@mistralai/mistralai/models/components'
import {
SDKError,
HTTPValidationError,
ResponseValidationError,
HTTPClientError,
} from '@mistralai/mistralai/models/errors'
import { ModelId } from 'src/schemas'
const MistralAPIErrorSchema = z.object({
error: z
.object({
message: z.string(),
type: z.string().optional(),
code: z.string().optional(),
})
.optional(),
message: z.string().optional(), // Some errors might have message at root
detail: z
.array(
z.object({
loc: z.array(z.union([z.string(), z.number()])),
msg: z.string(),
type: z.string(),
})
)
.optional(), // For 422 validation errors
})
export async function generateContent(
input: llm.GenerateContentInput,
mistral: Mistral,
logger: IntegrationLogger,
params: {
models: Record<ModelId, llm.ModelDetails>
defaultModel: ModelId
}
): Promise<llm.GenerateContentOutput> {
const modelId = (input.model?.id || params.defaultModel) as ModelId
const model = params.models[modelId]
if (!model) {
throw new InvalidPayloadError(
`Model ID "${modelId}" is not allowed, supported model IDs are: ${Object.keys(params.models).join(', ')}`
)
}
if (input.messages.length === 0 && !input.systemPrompt) {
throw new InvalidPayloadError('At least one message or a system prompt is required')
}
if (input.maxTokens && input.maxTokens > model.output.maxTokens) {
throw new InvalidPayloadError(
`maxTokens must be less than or equal to ${model.output.maxTokens} for model ID "${modelId}`
)
}
if (input.responseFormat === 'json_object') {
input.systemPrompt =
(input.systemPrompt || '') +
'\n\nYour response must always be in valid JSON format and expressed as a JSON object.'
}
const messages: Messages[] = []
// Add system prompt
if (input.systemPrompt) {
messages.unshift({
role: 'system',
content: input.systemPrompt,
})
}
for (const message of input.messages) {
messages.push(mapToMistralMessage(message))
}
const request: ChatCompletionRequest = {
model: modelId,
maxTokens: input.maxTokens ?? model.output.maxTokens,
temperature: input.temperature,
topP: input.topP,
stop: input.stopSequences,
metadata: {
user_id: input.userId,
},
tools: mapToMistralTools(input),
toolChoice: mapToMistralToolChoice(input.toolChoice),
messages,
}
if (input.debug) {
logger.forBot().info('Request being sent to Mistral: ' + JSON.stringify(request, null, 2))
}
let response: ChatCompletionResponse | undefined
try {
response = await mistral.chat.complete(request)
} catch (thrown: unknown) {
// Validation errors (422)
if (thrown instanceof HTTPValidationError) {
// err has: statusCode, body, detail[]
if (thrown.detail && thrown.detail.length > 0) {
const validationMessages = thrown.detail.map((d) => `${d.loc.join('.')}: ${d.msg}`).join('; ')
if (input.debug) {
logger.forBot().error(`Mistral validation errors: ${JSON.stringify(thrown.detail, null, 2)}`)
}
throw llm.createUpstreamProviderFailedError(
thrown,
`Mistral validation error (${thrown.statusCode}): ${validationMessages}`
)
}
}
// General SDK/API errors
if (thrown instanceof SDKError) {
let errorMessage = thrown.message
// parse body for more details
try {
const parsedBody = JSON.parse(thrown.body)
const parsedError = MistralAPIErrorSchema.safeParse(parsedBody)
if (parsedError.success && parsedError.data.error) {
errorMessage = parsedError.data.error.message
input.debug && logger.forBot().error(`Mistral API error: ${JSON.stringify(parsedError.data, null, 2)}`)
const errorType = parsedError.data.error.type ? ` (${parsedError.data.error.type})` : ''
throw llm.createUpstreamProviderFailedError(
thrown,
`Mistral error ${thrown.statusCode}${errorType}: ${errorMessage}`
)
}
} catch (parseErr) {
const parseErrorMessage = parseErr instanceof Error ? parseErr.message : String(parseErr)
// use basic info
if (input.debug) {
logger.forBot().warn(`Could not parse Mistral error body: ${thrown.body}, parse error: ${parseErrorMessage}`)
}
}
throw llm.createUpstreamProviderFailedError(thrown, `Mistral error ${thrown.statusCode}: ${errorMessage}`)
}
// Response validation errors
if (thrown instanceof ResponseValidationError) {
// Response from Mistral was invalid/unexpected format
if (input.debug) {
logger.forBot().error(`Mistral response validation error: ${thrown.message}`)
}
throw llm.createUpstreamProviderFailedError(thrown, `Mistral response validation error: ${thrown.message}`)
}
// Network/client errors
if (thrown instanceof HTTPClientError) {
if (input.debug) {
logger.forBot().error(`Mistral client error (${thrown.name}): ${thrown.message}`)
}
throw llm.createUpstreamProviderFailedError(thrown, `Mistral client error (${thrown.name}): ${thrown.message}`)
}
// unknown errors
if (input.debug) {
logger.forBot().error(`Unexpected error calling Mistral: ${JSON.stringify(thrown, null, 2)}`)
}
const error = thrown instanceof Error ? thrown : Error(String(thrown))
throw llm.createUpstreamProviderFailedError(error, `Mistral error: ${error.message}`)
} finally {
if (input.debug && response) {
logger.forBot().info('Response received from Mistral: ' + JSON.stringify(response, null, 2))
}
}
// fallback to zero, as it's done in the OpenAI integration
const inputTokens = response.usage?.promptTokens ?? 0
const outputTokens = response.usage?.completionTokens ?? 0
const inputCost = calculateTokenCost(model.input.costPer1MTokens, inputTokens)
const outputCost = calculateTokenCost(model.output.costPer1MTokens, outputTokens)
const cost = inputCost + outputCost
return {
id: response.id,
provider: 'mistral-ai',
model: response.model,
choices: response.choices.map((choice) => ({
role: 'assistant',
// TODO: Investigate showing images, for now it's not supported by any other provider
type: 'text', // Mistral can return multimodal content, but we extract text only,
content: extractTextContent(choice.message.content),
index: choice.index,
stopReason: mapToStopReason(choice.finishReason),
toolCalls: mapFromMistralToolCalls(choice.message.toolCalls, logger),
})),
usage: {
inputTokens,
inputCost,
outputTokens,
outputCost,
},
botpress: {
cost, // DEPRECATED
},
}
}
function mapToMistralMessage(message: llm.Message): Messages {
// Handle special messages where the role is overridden (tool calls)
if (message.type === 'tool_result') {
if (!message.toolResultCallId) {
throw new InvalidPayloadError('`toolResultCallId` is required when message type is "tool_result"')
}
return {
role: 'tool',
toolCallId: message.toolResultCallId,
content: message.content as string,
}
} else if (message.type === 'tool_calls') {
if (!message.toolCalls || message.toolCalls.length === 0) {
throw new InvalidPayloadError('`toolCalls` must contain at least one tool call when type is "tool_calls"')
}
return {
role: 'assistant',
toolCalls: message.toolCalls.map(mapToMistralToolCall),
// content can be omitted or null for tool call messages
}
}
// Handle regular messages by role
switch (message.role) {
case 'user':
case 'assistant':
return mapStandardMessage(message)
default:
throw new InvalidPayloadError(`Message role "${message.role}" is not supported`)
}
}
function mapStandardMessage(message: llm.Message): Messages {
if (message.type === 'text') {
if (typeof message.content !== 'string') {
throw new InvalidPayloadError('`content` must be a string when message type is "text"')
}
return {
role: message.role,
content: message.content,
}
}
if (message.type === 'multipart') {
if (!Array.isArray(message.content)) {
throw new InvalidPayloadError('`content` must be an array when message type is "multipart"')
}
return {
role: message.role,
content: mapMultipartContent(message.content),
}
}
throw new InvalidPayloadError(`Message type "${message.type}" is not supported for ${message.role} messages`)
}
/** Map multipart content into Mistral (ContentChunk) format */
function mapMultipartContent(content: NonNullable<llm.Message['content']>): ContentChunk[] {
if (typeof content === 'string') {
throw new InvalidPayloadError('Content must be an array for multipart messages')
}
const mistralContent: ContentChunk[] = []
for (const part of content) {
if (part.type === 'text') {
if (!part.text) {
throw new InvalidPayloadError('`text` is required when part type is "text"')
}
mistralContent.push({
type: 'text',
text: part.text,
})
} else if (part.type === 'image') {
if (!part.url) {
throw new InvalidPayloadError('`url` is required when part type is "image"')
}
mistralContent.push({
type: 'image_url',
imageUrl: part.url,
})
}
}
return mistralContent
}
function mapToMistralTools(input: llm.GenerateContentInput): Tool[] | undefined {
if (input.toolChoice?.type === 'none') {
// Don't return any tools if tool choice was to not use any tools
return []
}
const mistralTools = input.tools as Tool[] | undefined
// note: don't send an empty tools array
return mistralTools?.length ? mistralTools : undefined
}
function mapToMistralToolCall(toolCall: llm.ToolCall): ToolCall {
return {
id: toolCall.id,
type: 'function',
function: {
name: toolCall.function.name,
// Mistral expects a JSON string, not an object
arguments: JSON.stringify(toolCall.function.arguments),
},
}
}
function mapToMistralToolChoice(
toolChoice: llm.GenerateContentInput['toolChoice']
): ToolChoice | ToolChoiceEnum | undefined {
if (!toolChoice) {
return undefined
}
switch (toolChoice.type) {
case 'any':
case 'auto':
case 'none':
return <ToolChoiceEnum>toolChoice.type
case 'specific':
return <ToolChoice>{
type: 'function',
function: {
name: toolChoice.functionName,
},
}
default:
return undefined
}
}
function calculateTokenCost(costPer1MTokens: number, tokenCount: number) {
return (costPer1MTokens / 1_000_000) * tokenCount
}
function mapToStopReason(mistralFinishReason: FinishReason): llm.GenerateContentOutput['choices'][0]['stopReason'] {
switch (mistralFinishReason) {
case 'stop':
return 'stop'
case 'length':
case 'model_length':
return 'max_tokens'
case 'tool_calls':
return 'tool_calls'
case 'error':
return 'other'
default:
return 'other'
}
}
function mapFromMistralToolCalls(
mistralToolCalls: ToolCall[] | null | undefined,
logger: IntegrationLogger
): llm.ToolCall[] | undefined {
if (!mistralToolCalls || mistralToolCalls.length === 0) {
return undefined
}
return mistralToolCalls.reduce((toolCalls, mistralToolCall) => {
if (!mistralToolCall.id) {
logger.forBot().warn('Mistral returned tool call without ID, skipping')
return toolCalls
}
const toolType = mistralToolCall.type || 'function' // Default to 'function' if not provided
if (toolType !== 'function') {
logger.forBot().warn(`Unsupported tool call type "${toolType}" from Mistral, skipping`)
return toolCalls
}
let toolCallArguments: llm.ToolCall['function']['arguments']
const rawArguments = mistralToolCall.function.arguments
// arguments can be either string or json
if (typeof rawArguments === 'string') {
try {
toolCallArguments = JSON.parse(rawArguments)
} catch (err) {
logger
.forBot()
.warn(
`Mistral returned invalid JSON for tool call "${mistralToolCall.function.name}" arguments. ` +
`Using null instead. Error: ${err}`
)
toolCallArguments = null
}
} else if (typeof rawArguments === 'object' && rawArguments !== null) {
toolCallArguments = rawArguments
} else {
logger
.forBot()
.warn(
`Mistral returned unexpected type for tool call "${mistralToolCall.function.name}" arguments: ${typeof rawArguments}. Using null instead.`
)
toolCallArguments = null
}
toolCalls.push({
id: mistralToolCall.id,
type: 'function',
function: {
name: mistralToolCall.function.name,
arguments: toolCallArguments,
},
})
return toolCalls
}, [] as llm.ToolCall[])
}
function extractTextContent(content: string | ContentChunk[] | null | undefined): string | null {
if (!content) {
return null
}
if (typeof content === 'string') {
return content
}
// content is ContentChunk[] - extract only text chunks
return (
content
.filter((chunk): chunk is Extract<ContentChunk, { type: 'text' }> => chunk.type === 'text')
.map((chunk) => chunk.text)
.join('\n\n') || null
)
}
+157
View File
@@ -0,0 +1,157 @@
import { llm } from '@botpress/common'
import { Mistral } from '@mistralai/mistralai'
import { generateContent } from './actions/generate-content'
import { DefaultModel, ModelId } from './schemas'
import * as bp from '.botpress'
const mistral = new Mistral({ apiKey: bp.secrets.MISTRAL_API_KEY })
const LanguageModels: Record<ModelId, llm.ModelDetails> = {
// Reference: https://docs.mistral.ai/getting-started/models
'mistral-large-2512': {
name: 'Mistral Large 3',
description:
'Mistral Large 3, is a state-of-the-art, open-weight, general-purpose multimodal model with a granular Mixture-of-Experts architecture. It features 41B active parameters and 675B total parameters.',
tags: [
/* TODO: Add tags */
],
input: {
costPer1MTokens: 0.5,
maxTokens: 256_000,
},
output: {
costPer1MTokens: 1.5,
maxTokens: 4096,
},
},
'mistral-medium-2508': {
name: 'Mistral Medium 3.1',
description: 'Frontier-class multimodal model released August 2025. Improving tone and performance.',
tags: [
/* TODO: Add tags */
],
input: {
costPer1MTokens: 0.4,
maxTokens: 128_000,
},
output: {
costPer1MTokens: 2,
maxTokens: 4096,
},
},
'mistral-small-2506': {
name: 'Mistral Small 3.2',
description: 'An update to the previous small model, released June 2025.',
tags: [
/* TODO: Add tags */
],
input: {
costPer1MTokens: 0.1,
maxTokens: 128_000,
},
output: {
costPer1MTokens: 0.3,
maxTokens: 4096,
},
},
'ministral-14b-2512': {
name: 'Ministral 3 14B',
description:
'Ministral 3 14B is the largest model in the Ministral 3 family, offering state-of-the-art capabilities and performance comparable to its larger Mistral Small 3.2 24B counterpart.',
tags: [
/* TODO: Add tags */
],
input: {
costPer1MTokens: 0.2,
maxTokens: 256_000,
},
output: {
costPer1MTokens: 0.2,
maxTokens: 4096,
},
},
'ministral-8b-2512': {
name: 'Ministral 3 8B',
description:
'Ministral 3 8B is a powerful and efficient model in the Ministral 3 family, offering best-in-class text and vision capabilities.',
tags: [
/* TODO: Add tags */
],
input: {
costPer1MTokens: 0.15,
maxTokens: 256_000,
},
output: {
costPer1MTokens: 0.15,
maxTokens: 4096,
},
},
'ministral-3b-2512': {
name: 'Ministral 3 3B',
description:
'Ministral 3 3B is the smallest and most efficient model in the Ministral 3 family, offering robust language and vision capabilities in a compact package.',
tags: [
/* TODO: Add tags */
],
input: {
costPer1MTokens: 0.1,
maxTokens: 256_000,
},
output: {
costPer1MTokens: 0.1,
maxTokens: 4096,
},
},
'magistral-medium-2509': {
name: 'Magistral Medium 1.2',
description: 'Frontier-class multimodal reasoning model update of September 2025.',
tags: [
/* TODO: Add tags */
],
input: {
costPer1MTokens: 2,
maxTokens: 128_000,
},
output: {
costPer1MTokens: 5,
maxTokens: 4096,
},
},
'magistral-small-2509': {
name: 'Magistral Small 1.2',
description: 'Small multimodal reasoning model update of September 2025.',
tags: [
/* TODO: Add tags */
],
input: {
costPer1MTokens: 0.5,
maxTokens: 128_000,
},
output: {
costPer1MTokens: 1.5,
maxTokens: 4096,
},
},
}
export default new bp.Integration({
register: async () => {},
unregister: async () => {},
actions: {
generateContent: async ({ input, logger, metadata }) => {
const output = await generateContent(<llm.GenerateContentInput>input, mistral, logger, {
models: LanguageModels,
defaultModel: DefaultModel,
})
metadata.setCost(output.botpress.cost)
return output
},
listLanguageModels: async ({}) => {
return {
models: Object.entries(LanguageModels).map(([id, model]) => ({ id: <ModelId>id, ...model })),
}
},
},
channels: {},
handler: async () => {},
})
+19
View File
@@ -0,0 +1,19 @@
import { z } from '@botpress/sdk'
export type ModelId = z.infer<typeof ModelId>
export const DefaultModel: ModelId = 'mistral-large-2512'
export const ModelId = z
.enum([
'mistral-large-2512',
'mistral-medium-2508',
'mistral-small-2506',
'ministral-14b-2512',
'ministral-8b-2512',
'ministral-3b-2512',
'magistral-medium-2509',
'magistral-small-2509',
])
.describe('Model to use for content generation')
.placeholder(DefaultModel)
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"paths": { "*": ["./*"] },
"outDir": "dist"
},
"include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"]
}
+2
View File
@@ -0,0 +1,2 @@
import config from '../../vitest.config'
export default config