File size: 5,307 Bytes
3632834
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// MiniCPM5 native XML uses Python literals for typed parameters. No eval/Function.
export function parseMiniCpmParamValue(input, schema = {}) {
  const raw = String(input ?? '');
  const cdata = /^\s*<!\[CDATA\[([\s\S]*?)\]\]>\s*$/.exec(raw);
  let decoded=cdata ? cdata[1] : raw;
  if(!cdata) {
    if(raw.includes('<')) throw Error('Malformed XML parameter: use CDATA for literal markup');
    decoded=raw.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos);/gi,(_,entity)=>{
      if(entity.startsWith('#')) { const n=entity[1].toLowerCase()==='x'?parseInt(entity.slice(2),16):parseInt(entity.slice(1),10);return String.fromCodePoint(n); }
      return {amp:'&',lt:'<',gt:'>',quot:'"',apos:"'"}[entity];
    });
    if(/&(?!(?:#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos);)/i.test(raw)) throw Error('Unescaped XML ampersand');
  }
  if(schema.type==='string') return decoded;
  const text = decoded.trim();
  if (!text) return '';
  let i = 0;
  const ws = () => { while (i < text.length && /\s/u.test(text[i])) i++; };
  const take = char => { ws(); if (text[i++] !== char) throw Error('Invalid literal'); };
  function value(depth = 0) {
    if (depth > 32) throw Error('Literal nesting limit');
    ws();
    const c = text[i];
    if (c === "'" || c === '"') {
      const quote = text[i++]; let result = '';
      while (i < text.length) {
        const ch = text[i++];
        if (ch === quote) return result;
        if (ch !== '\\') { result += ch; continue; }
        const e = text[i++];
        const simple = { '\\':'\\', "'":"'", '"':'"', n:'\n', r:'\r', t:'\t', b:'\b', f:'\f', '/':'/' };
        if (Object.hasOwn(simple,e)) { result += simple[e]; continue; }
        const width = e === 'u' ? 4 : e === 'x' ? 2 : 0;
        const hex = text.slice(i,i+width);
        if (!width || !new RegExp(`^[a-fA-F0-9]{${width}}$`).test(hex)) throw Error('Invalid escape');
        result += String.fromCharCode(parseInt(hex,16)); i += width;
      }
      throw Error('Unterminated string');
    }
    if (c === '[' || c === '{') {
      i++; const array = c === '['; const end = array ? ']' : '}';
      const result = array ? [] : Object.create(null);
      ws(); if (text[i] === end) { i++; return array ? result : {...result}; }
      while (i < text.length) {
        if (array) result.push(value(depth+1));
        else {
          ws(); if (!['"',"'"].includes(text[i])) throw Error('Expected string key');
          const key = value(depth+1);
          if (Object.hasOwn(result,key)) throw Error('Duplicate key');
          take(':'); result[key] = value(depth+1);
        }
        ws(); if (text[i] === end) { i++; return array ? result : {...result}; }
        take(','); ws();
        if (text[i] === end) { i++; return array ? result : {...result}; }
      }
      throw Error('Unterminated container');
    }
    const scalar = /^(True|False|None|true|false|null)(?![\w])/u.exec(text.slice(i));
    if (scalar) { i += scalar[0].length; return {True:true,False:false,None:null,true:true,false:false,null:null}[scalar[0]]; }
    const number = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/u.exec(text.slice(i));
    if (number) { i += number[0].length; const result=Number(number[0]); if(Number.isFinite(result)) return result; }
    throw Error('Not a literal');
  }
  try { const result=value(); ws(); if(i===text.length) return result; } catch { /* native strings are also unquoted */ }
  return text;
}

// Strict standalone adapter. Keep downstream tool schema/approval validation in place.
export function parseMiniCpmToolCalls(text, toolsOrNames) {
  const tools=Array.isArray(toolsOrNames)?toolsOrNames:[];
  const allowedNames=Array.isArray(toolsOrNames)?new Set(tools.map(t=>t.function.name)):toolsOrNames;
  const schemas=new Map(tools.map(t=>[t.function.name,t.function.parameters || {}]));
  const source=String(text ?? '').trim();
  if (!source || source.length>10000 || !(allowedNames instanceof Set)) return [];
  // Native MiniCPM may explain an error before emitting the XML tool call.
  // Accept that prose, but never skip over a malformed XML call or parameter.
  const start=source.indexOf('<function');
  if(start<0 || /<\/?(?:function|param)\b/.test(source.slice(0,start))) return [];
  const calls=[]; let cursor=start;
  const functions=/<function\s+name=["']([A-Za-z_]\w*)["']\s*>([\s\S]*?)<\/function>/gy;
  const params=/<param\s+name=["']([A-Za-z_]\w*)["']\s*>([\s\S]*?)<\/param>/gy;
  while(cursor<source.length) {
    while (/\s/u.test(source[cursor] || '')) cursor++;
    if(cursor===source.length) break;
    functions.lastIndex=cursor; const fn=functions.exec(source);
    if(!fn || !allowedNames.has(fn[1])) return [];
    const args=Object.create(null); let pos=0;
    while(pos<fn[2].length) {
      while(/\s/u.test(fn[2][pos] || '')) pos++;
      if(pos===fn[2].length) break;
      params.lastIndex=pos; const param=params.exec(fn[2]);
      if(!param || Object.hasOwn(args,param[1])) return [];
      try { args[param[1]]=parseMiniCpmParamValue(param[2],schemas.get(fn[1])?.properties?.[param[1]]); }
      catch { return []; }
      pos=params.lastIndex;
    }
    calls.push({id:`compass_call_${Date.now()}_${calls.length}`,type:'function',function:{name:fn[1],arguments:JSON.stringify(args)}});
    cursor=functions.lastIndex;
  }
  return calls;
}