diff --git a/.cspell-wordlist.txt b/.cspell-wordlist.txt index 411a9c8d64..5cc8b90cb6 100644 --- a/.cspell-wordlist.txt +++ b/.cspell-wordlist.txt @@ -432,3 +432,5 @@ yunjian yunxi yunxia yunyang +apos +minicpm diff --git a/apps/nlp/constants/llm.ts b/apps/nlp/constants/llm.ts index da2ede1919..c2ba59819d 100644 --- a/apps/nlp/constants/llm.ts +++ b/apps/nlp/constants/llm.ts @@ -284,6 +284,77 @@ export function parseLfmToolCalls(text: string): llm.ToolParserResult | undefine return { toolCalls, textContent: textContent || undefined }; } +function decodeMiniCpmArgument(value: string): string { + const trimmed = value.trim(); + if (trimmed.startsWith('')) { + return trimmed.slice(9, -3); + } + const entities: Record = { + amp: '&', + lt: '<', + gt: '>', + quot: '"', + apos: "'", + }; + return trimmed.replace(/&(#x[\da-f]+|#\d+|amp|lt|gt|quot|apos);/gi, (_, entity: string) => { + if (entity.startsWith('#')) { + const code = entity.slice(1).toLowerCase(); + return String.fromCodePoint( + code.startsWith('x') ? parseInt(code.slice(1), 16) : Number(code) + ); + } + return entities[entity.toLowerCase()]!; + }); +} + +export function parseMiniCpmToolCalls(text: string): llm.ToolParserResult | undefined { + const thinkingEnd = text.lastIndexOf(''); + if (thinkingEnd === -1) return undefined; + const answer = text.slice(thinkingEnd + ''.length); + const callRegex = + /((?:|[\s\S])*?)<\/function>/g; + const toolCalls: llm.ToolCall[] = []; + + for (const match of answer.matchAll(callRegex)) { + const name = match[2]!; + const body = match[3]!; + const tool = TOOLS.find((candidate) => candidate.function.name === name); + if (!tool) return undefined; + const params = tool.function.parameters; + const paramRegex = + /((?:|[\s\S])*?)<\/param>/g; + const args: Record = {}; + + try { + for (const param of body.matchAll(paramRegex)) { + const key = param[2]!; + const schema = params?.properties?.[key] as { type?: string } | undefined; + if (!schema || Object.prototype.hasOwnProperty.call(args, key)) return undefined; + const value = decodeMiniCpmArgument(param[3]!); + const parsed = schema.type === 'string' ? value : JSON.parse(value); + if (schema.type === 'number' && (typeof parsed !== 'number' || !Number.isFinite(parsed))) { + return undefined; + } + args[key] = parsed; + } + } catch { + return undefined; + } + if (body.replace(paramRegex, '').trim()) return undefined; + if (params?.required?.some((key) => !Object.prototype.hasOwnProperty.call(args, key))) { + return undefined; + } + toolCalls.push({ type: 'function', function: { name, arguments: args } }); + } + + if (toolCalls.length === 0) return undefined; + const textContent = answer + .replace(callRegex, '') + .replace(/<\|im_end\|>/g, '') + .trim(); + return { toolCalls, textContent: textContent || undefined }; +} + export const QWEN_TOOL_STOP_REGEX = /(?:<\/tool_call>|<\|im_end\|>)/; export function parseQwenToolCalls(text: string): llm.ToolParserResult | undefined { @@ -375,6 +446,14 @@ export function parseLlamaToolCalls(text: string): llm.ToolParserResult | undefi // --- Model Family Generation Configurations --- +/** Sampling defaults recommended by the MiniCPM5-2B model card. */ +export const MINICPM5_GENERATION_CONFIG: llm.LLMGenerationConfig = { + temperature: 1.0, + topP: 0.95, + minP: 0.0, + maxNewTokens: 512, +}; + /** * Liquid AI LFM 2.5 recommended generation parameters. * Sampling parameters are specified in the official Liquid AI GGUF model cards @@ -532,6 +611,11 @@ export const LFM_TOOL_OPTS: LLMToolOpts = { parseToolCalls: parseLfmToolCalls, }; +export const MINICPM5_TOOL_OPTS: LLMToolOpts = { + tools: TOOLS, + parseToolCalls: parseMiniCpmToolCalls, +}; + export const GEMMA_TOOL_OPTS: LLMToolOpts = { tools: TOOLS, parseToolCalls: parseGemmaToolCalls, @@ -571,6 +655,27 @@ export interface LLMModelConfig { // --- Quantized LLM Models List --- export const LLM_MODELS: LLMModelConfig[] = [ + // MiniCPM5 + { + id: 'minicpm-5-2b-xnnpack', + name: 'MiniCPM5 2B', + backend: 'xnnpack', + model: models.llm.MINICPM5_2B.XNNPACK_8DA4W, + generationConfig: MINICPM5_GENERATION_CONFIG, + systemPrompt: `${DEFAULT_SYSTEM_PROMPT}. Always reply in the language of the user's latest message.`, + toolOpts: MINICPM5_TOOL_OPTS, + }, + { + id: 'minicpm-5-2b-mlx', + name: 'MiniCPM5 2B', + backend: 'mlx', + model: models.llm.MINICPM5_2B.MLX_INT4, + generationConfig: MINICPM5_GENERATION_CONFIG, + systemPrompt: `${DEFAULT_SYSTEM_PROMPT}. Always reply in the language of the user's latest message.`, + toolOpts: MINICPM5_TOOL_OPTS, + iosOnly: true, + }, + // LFM 2.5 family { id: 'lfm-2.5-2.6b-xnnpack', @@ -1107,7 +1212,7 @@ export const LLM_MODELS: LLMModelConfig[] = [ backend: 'xnnpack', model: models.llm.BIELIK_V3_1_5B.XNNPACK_8DA4W, generationConfig: BIELIK_V3_GENERATION_CONFIG, - // eslint-disable-next-line @cspell/spellchecker + // cspell:disable-next-line systemPrompt: 'Jesteś pomocnym asystentem AI mówiącym po polsku.', stopRegex: undefined, toolOpts: undefined, diff --git a/docs/docs/02-extensions/natural-language/02-llm-chat-and-generation.md b/docs/docs/02-extensions/natural-language/02-llm-chat-and-generation.md index 239d7ead0b..a5f1252494 100644 --- a/docs/docs/02-extensions/natural-language/02-llm-chat-and-generation.md +++ b/docs/docs/02-extensions/natural-language/02-llm-chat-and-generation.md @@ -381,6 +381,7 @@ The library provides ready-to-use models from the [Software Mansion HuggingFace | **Microsoft Phi-4 Mini** | [`3.8B`](../../06-api-reference/variables/models.md#llmphi4_mini) | 2.62 GB – 7.15 GB | XNNPACK (CPU), MLX (Apple) | High-density reasoning model for STEM problem solving & coding. | | **HuggingFace SmolLM2** | [`135M`](../../06-api-reference/variables/models.md#llmsmollm2_135m), [`360M`](../../06-api-reference/variables/models.md#llmsmollm2_360m), [`1.7B`](../../06-api-reference/variables/models.md#llmsmollm2_1_7b) | 158 MB – 1.83 GB | XNNPACK (CPU), MLX (Apple) | Smallest footprint family, for constrained devices and fast drafting. | | **SpeakLeash Bielik v3** | [`1.5B`](../../06-api-reference/variables/models.md#llmbielik_v3_1_5b) | 923 MB – 2.97 GB | XNNPACK (CPU) | Bilingual Polish & English instruction model. | +| **OpenBMB MiniCPM5** | [`2B`](../../06-api-reference/variables/models.md#llmminicpm5_2b) | 1.36 GB – 1.42 GB | XNNPACK (CPU), MLX (Apple) | Compact reasoning model with thinking and tool calling. | :::tip Using Custom Models To use your own fine-tuned LLM `.pte` model, pass an [`LLMModel`](../../06-api-reference/type-aliases/LLMModel.md) configuration object to [`useLLMChatSession`](../../06-api-reference/functions/useLLMChatSession.md) or [`createLLMChatSession`](../../06-api-reference/functions/createLLMChatSession.md): diff --git a/packages/react-native-executorch/src/models.ts b/packages/react-native-executorch/src/models.ts index a5e1fe322d..484011389e 100644 --- a/packages/react-native-executorch/src/models.ts +++ b/packages/react-native-executorch/src/models.ts @@ -2098,6 +2098,19 @@ const PPOCRV6_SMALL_VULKAN_FP16: PaddleOcrModel = { // ============================================================================= // LLMs // ============================================================================= +const MINICPM5_BASE_URL = `${BASE_URL}-minicpm-5/${NEXT_VERSION_TAG}`; + +const MINICPM5_2B_XNNPACK_8DA4W: LLMModel = { + modelPath: `${MINICPM5_BASE_URL}/xnnpack/minicpm_5_2b_xnnpack_8da4w.pte`, + tokenizerPath: `${MINICPM5_BASE_URL}/tokenizer.json`, + tokenizerConfigPath: `${MINICPM5_BASE_URL}/tokenizer_config.json`, +}; +const MINICPM5_2B_MLX_INT4: LLMModel = { + modelPath: `${MINICPM5_BASE_URL}/mlx/minicpm_5_2b_mlx_4w.pte`, + tokenizerPath: `${MINICPM5_BASE_URL}/tokenizer.json`, + tokenizerConfigPath: `${MINICPM5_BASE_URL}/tokenizer_config.json`, +}; + const LFM2_5_BASE_URL = `${BASE_URL}-lfm-2.5/${NEXT_VERSION_TAG}`; const LFM2_5_1_2B_XNNPACK_8DA4W: LLMModel = { @@ -3225,6 +3238,11 @@ export const models = { * text generation, and reasoning. */ llm: { + /** OpenBMB MiniCPM5 2B chat model with an 8K context window. */ + MINICPM5_2B: variants({ + XNNPACK_8DA4W: MINICPM5_2B_XNNPACK_8DA4W, + MLX_INT4: MINICPM5_2B_MLX_INT4, + }), /** * Liquid AI LFM 2.5 1.2B general-purpose hybrid language model. Built on * the Liquid Foundation Model architecture for low memory bandwidth usage,