pi-free 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -3
- package/README.md +32 -4
- package/banner.svg +1 -1
- package/config.ts +644 -629
- package/constants.ts +4 -0
- package/index.ts +380 -378
- package/lib/built-in-toggle.ts +0 -40
- package/lib/probe-cache.ts +8 -0
- package/lib/provider-probe.ts +15 -0
- package/package.json +5 -5
- package/provider-helper.ts +1 -25
- package/providers/bai/bai.ts +232 -0
- package/providers/cline/cline-xml-bridge.ts +631 -105
- package/providers/cline/cline.ts +0 -23
- package/providers/codestral/codestral.ts +0 -11
- package/providers/dynamic-built-in/index.ts +12 -20
- package/providers/kilo/kilo.ts +2 -19
- package/providers/ollama/ollama.ts +12 -12
- package/providers/routeway/routeway.ts +10 -0
- package/providers/tokenrouter/tokenrouter.ts +634 -378
|
@@ -46,6 +46,73 @@ function normalizeApiModelId(modelId: string): string {
|
|
|
46
46
|
: modelId;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Some MiMo/Cline models emit XML tags wrapped in Unicode math-italic
|
|
51
|
+
* characters that spell out "anthml:" before the real tag name:
|
|
52
|
+
* <𝑎𝑛𝑡𝑚𝑙:thinking>...</𝑎𝑛𝑡𝑚𝑙:thinking>
|
|
53
|
+
* <𝑎𝑛𝑡𝑚𝑙:read_file>...</𝑎𝑛𝑡𝑚𝑙:read_file>
|
|
54
|
+
*
|
|
55
|
+
* This function strips the Unicode-decorated prefix so the rest of the
|
|
56
|
+
* parser sees standard ASCII XML tags.
|
|
57
|
+
*/
|
|
58
|
+
function normalizeDecoratedXmlTags(text: string): string {
|
|
59
|
+
const parts: string[] = [];
|
|
60
|
+
let cursor = 0;
|
|
61
|
+
|
|
62
|
+
while (cursor < text.length) {
|
|
63
|
+
const ltIndex = text.indexOf("<", cursor);
|
|
64
|
+
if (ltIndex === -1) {
|
|
65
|
+
parts.push(text.slice(cursor));
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
parts.push(text.slice(cursor, ltIndex));
|
|
70
|
+
let contentStart = ltIndex + 1;
|
|
71
|
+
let prefix = "<";
|
|
72
|
+
|
|
73
|
+
// Handle closing tags: </𝑎𝑛𝑡𝑚𝑙:thinking> → </thinking>
|
|
74
|
+
if (contentStart < text.length && text[contentStart] === "/") {
|
|
75
|
+
prefix = "</";
|
|
76
|
+
contentStart += 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const gtIndex = text.indexOf(">", contentStart);
|
|
80
|
+
const colonIndex = text.indexOf(":", contentStart);
|
|
81
|
+
const spaceIndex = text.indexOf(" ", contentStart);
|
|
82
|
+
if (
|
|
83
|
+
colonIndex === -1 ||
|
|
84
|
+
colonIndex === contentStart ||
|
|
85
|
+
(gtIndex !== -1 && colonIndex > gtIndex) ||
|
|
86
|
+
(spaceIndex !== -1 && spaceIndex < colonIndex)
|
|
87
|
+
) {
|
|
88
|
+
parts.push(prefix);
|
|
89
|
+
cursor = contentStart;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Strip non-ASCII bytes between prefix and : to undo Unicode-decorated
|
|
94
|
+
// prefixes like <𝑎𝑛𝑡𝑚𝑙:thinking> → <thinking>.
|
|
95
|
+
let hasNonAscii = false;
|
|
96
|
+
for (let i = contentStart; i < colonIndex; i++) {
|
|
97
|
+
if (text.charCodeAt(i) > 127) {
|
|
98
|
+
hasNonAscii = true;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (hasNonAscii) {
|
|
104
|
+
parts.push(prefix);
|
|
105
|
+
cursor = colonIndex + 1;
|
|
106
|
+
} else {
|
|
107
|
+
// No decorated prefix - emit < and re-include everything after it
|
|
108
|
+
parts.push("<");
|
|
109
|
+
cursor = ltIndex + 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return parts.join("");
|
|
114
|
+
}
|
|
115
|
+
|
|
49
116
|
function xmlEscape(value: unknown): string {
|
|
50
117
|
return String(value)
|
|
51
118
|
.replaceAll("&", "&")
|
|
@@ -248,10 +315,25 @@ function replaceInFileBridge(tool?: Tool): ToolBridge {
|
|
|
248
315
|
description:
|
|
249
316
|
tool?.description ?? "Edit a file using Cline SEARCH/REPLACE blocks",
|
|
250
317
|
parameters: ["path", "diff"],
|
|
251
|
-
toRuntimeArgs: (args) =>
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
318
|
+
toRuntimeArgs: (args) => {
|
|
319
|
+
// Pi native <edit> form sends <edits>[{oldText,newText},...]</edits>
|
|
320
|
+
// as JSON. Cline <replace_in_file> form uses SEARCH/REPLACE <diff>.
|
|
321
|
+
if (Array.isArray(args.edits)) {
|
|
322
|
+
return {
|
|
323
|
+
path: stringArg(args, "path"),
|
|
324
|
+
edits: args.edits
|
|
325
|
+
.map((edit) => ({
|
|
326
|
+
oldText: stringArg(edit as Record<string, unknown>, "oldText"),
|
|
327
|
+
newText: stringArg(edit as Record<string, unknown>, "newText"),
|
|
328
|
+
}))
|
|
329
|
+
.filter((edit) => edit.oldText || edit.newText),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
path: stringArg(args, "path"),
|
|
334
|
+
edits: parseSearchReplaceBlocks(stringArg(args, "diff")),
|
|
335
|
+
};
|
|
336
|
+
},
|
|
255
337
|
fromRuntimeArgs: (args) => {
|
|
256
338
|
const edits = Array.isArray(args.edits)
|
|
257
339
|
? args.edits
|
|
@@ -291,6 +373,111 @@ function executeCommandBridge(tool?: Tool): ToolBridge {
|
|
|
291
373
|
};
|
|
292
374
|
}
|
|
293
375
|
|
|
376
|
+
type HeredocWriteCommand = {
|
|
377
|
+
path: string;
|
|
378
|
+
content: string;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
function shellSplitLine(line: string): string[] {
|
|
382
|
+
const tokens: string[] = [];
|
|
383
|
+
let current = "";
|
|
384
|
+
let quote: '"' | "'" | undefined;
|
|
385
|
+
|
|
386
|
+
for (let i = 0; i < line.length; i++) {
|
|
387
|
+
const char = line[i];
|
|
388
|
+
if (quote) {
|
|
389
|
+
if (char === quote) {
|
|
390
|
+
quote = undefined;
|
|
391
|
+
} else {
|
|
392
|
+
current += char;
|
|
393
|
+
}
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (char === '"' || char === "'") {
|
|
397
|
+
quote = char;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (char === " " || char === "\t") {
|
|
401
|
+
if (current) {
|
|
402
|
+
tokens.push(current);
|
|
403
|
+
current = "";
|
|
404
|
+
}
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
current += char;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (current) tokens.push(current);
|
|
411
|
+
return tokens;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function parseCatHeredocWriteCommand(
|
|
415
|
+
command: string,
|
|
416
|
+
): HeredocWriteCommand | undefined {
|
|
417
|
+
const normalized = command.replaceAll("\r\n", "\n").trim();
|
|
418
|
+
const lines = normalized.split("\n");
|
|
419
|
+
if (lines.length < 3) return undefined;
|
|
420
|
+
|
|
421
|
+
const tokens = shellSplitLine(lines[0].trim());
|
|
422
|
+
if (tokens[0] !== "cat") return undefined;
|
|
423
|
+
const redirectIndex = tokens.indexOf(">");
|
|
424
|
+
if (redirectIndex === -1) return undefined;
|
|
425
|
+
const path = tokens[redirectIndex + 1];
|
|
426
|
+
if (!path) return undefined;
|
|
427
|
+
|
|
428
|
+
let delimiter = "";
|
|
429
|
+
for (let i = redirectIndex + 2; i < tokens.length; i++) {
|
|
430
|
+
const token = tokens[i];
|
|
431
|
+
if (token === "<<") {
|
|
432
|
+
delimiter = tokens[i + 1] ?? "";
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
if (token.startsWith("<<")) {
|
|
436
|
+
delimiter = token.slice(2);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (!delimiter) return undefined;
|
|
441
|
+
|
|
442
|
+
let delimiterLine = -1;
|
|
443
|
+
for (let i = 1; i < lines.length; i++) {
|
|
444
|
+
if (lines[i].trim() === delimiter) {
|
|
445
|
+
delimiterLine = i;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (delimiterLine === -1) return undefined;
|
|
450
|
+
|
|
451
|
+
const trailing = lines
|
|
452
|
+
.slice(delimiterLine + 1)
|
|
453
|
+
.join("\n")
|
|
454
|
+
.trim();
|
|
455
|
+
if (trailing) {
|
|
456
|
+
const trailingLines = trailing.split("\n").filter((line) => line.trim());
|
|
457
|
+
if (trailingLines.length !== 1) return undefined;
|
|
458
|
+
const trailingTokens = shellSplitLine(trailingLines[0].trim());
|
|
459
|
+
if (trailingTokens.length !== 2 || trailingTokens[0] !== "cat") {
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
if (trailingTokens[1] !== path) return undefined;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
path,
|
|
467
|
+
content: lines.slice(1, delimiterLine).join("\n"),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function getWriteRuntimeToolName(
|
|
472
|
+
tools: Tool[] | undefined,
|
|
473
|
+
): string | undefined {
|
|
474
|
+
if ((tools ?? []).some((tool) => tool.name === "write_to_file")) {
|
|
475
|
+
return "write_to_file";
|
|
476
|
+
}
|
|
477
|
+
if ((tools ?? []).some((tool) => tool.name === "write")) return "write";
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
|
|
294
481
|
function listFilesBridge(): ToolBridge {
|
|
295
482
|
return {
|
|
296
483
|
remoteName: "list_files",
|
|
@@ -558,54 +745,98 @@ function pushTextFragment(textParts: string[], fragment: string): void {
|
|
|
558
745
|
textParts.push(trimmed);
|
|
559
746
|
}
|
|
560
747
|
|
|
748
|
+
type HiddenThoughtTag = {
|
|
749
|
+
open: string;
|
|
750
|
+
closes: string[];
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
const HIDDEN_THOUGHT_TAGS: HiddenThoughtTag[] = [
|
|
754
|
+
{ open: "<thinking>", closes: ["</thinking>"] },
|
|
755
|
+
// Some DeepSeek/Cline variants open with <think> but close with </thinking>.
|
|
756
|
+
{ open: "<think>", closes: ["</think>", "</thinking>"] },
|
|
757
|
+
// Compaction/summary artifacts can leak into Cline content as </summary>.
|
|
758
|
+
{ open: "<summary>", closes: ["</summary>"] },
|
|
759
|
+
// Cline may emit persistent issue-checking as hidden deliberation.
|
|
760
|
+
{
|
|
761
|
+
open: "<persistent_issue_checking>",
|
|
762
|
+
closes: ["</persistent_issue_checking>"],
|
|
763
|
+
},
|
|
764
|
+
];
|
|
765
|
+
|
|
766
|
+
const HIDDEN_THOUGHT_CLOSE_TAGS = Array.from(
|
|
767
|
+
new Set(HIDDEN_THOUGHT_TAGS.flatMap((tag) => tag.closes)),
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
function findNextHiddenOpenTag(
|
|
771
|
+
text: string,
|
|
772
|
+
from: number,
|
|
773
|
+
): { index: number; tag: HiddenThoughtTag } | null {
|
|
774
|
+
let best: { index: number; tag: HiddenThoughtTag } | null = null;
|
|
775
|
+
for (const tag of HIDDEN_THOUGHT_TAGS) {
|
|
776
|
+
const index = text.indexOf(tag.open, from);
|
|
777
|
+
if (index === -1) continue;
|
|
778
|
+
if (!best || index < best.index) best = { index, tag };
|
|
779
|
+
}
|
|
780
|
+
return best;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function findNextCloseTag(
|
|
784
|
+
text: string,
|
|
785
|
+
from: number,
|
|
786
|
+
closeTags: string[],
|
|
787
|
+
): { index: number; tag: string } | null {
|
|
788
|
+
let best: { index: number; tag: string } | null = null;
|
|
789
|
+
for (const tag of closeTags) {
|
|
790
|
+
const index = text.indexOf(tag, from);
|
|
791
|
+
if (index === -1) continue;
|
|
792
|
+
if (!best || index < best.index) best = { index, tag };
|
|
793
|
+
}
|
|
794
|
+
return best;
|
|
795
|
+
}
|
|
796
|
+
|
|
561
797
|
function extractThinkingXml(text: string): {
|
|
562
798
|
text: string;
|
|
563
799
|
thinking: string[];
|
|
564
800
|
} {
|
|
565
801
|
const thinking: string[] = [];
|
|
566
802
|
const parts: string[] = [];
|
|
567
|
-
const openTags = ["<thinking>", "<think>"];
|
|
568
|
-
const closeTag = "</thinking>";
|
|
569
803
|
let cursor = 0;
|
|
570
804
|
|
|
571
|
-
function findNextOpenTag(from: number): { index: number; tag: string } | null {
|
|
572
|
-
let best: { index: number; tag: string } | null = null;
|
|
573
|
-
for (const tag of openTags) {
|
|
574
|
-
const index = text.indexOf(tag, from);
|
|
575
|
-
if (index === -1) continue;
|
|
576
|
-
if (!best || index < best.index) best = { index, tag };
|
|
577
|
-
}
|
|
578
|
-
return best;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
805
|
while (cursor < text.length) {
|
|
582
|
-
const nextOpen =
|
|
806
|
+
const nextOpen = findNextHiddenOpenTag(text, cursor);
|
|
583
807
|
const openStart = nextOpen?.index ?? -1;
|
|
584
|
-
const
|
|
808
|
+
const nextClose = findNextCloseTag(text, cursor, HIDDEN_THOUGHT_CLOSE_TAGS);
|
|
809
|
+
const closeStart = nextClose?.index ?? -1;
|
|
585
810
|
|
|
586
|
-
if (
|
|
811
|
+
if (nextClose && (openStart === -1 || closeStart < openStart)) {
|
|
587
812
|
const danglingThinking = decodeXmlEntities(
|
|
588
813
|
text.slice(cursor, closeStart).trim(),
|
|
589
814
|
);
|
|
590
815
|
if (danglingThinking) thinking.push(danglingThinking);
|
|
591
|
-
cursor = closeStart +
|
|
816
|
+
cursor = closeStart + nextClose.tag.length;
|
|
592
817
|
continue;
|
|
593
818
|
}
|
|
594
819
|
|
|
595
820
|
if (openStart === -1 || !nextOpen) break;
|
|
596
821
|
parts.push(text.slice(cursor, openStart));
|
|
597
|
-
const valueStart = openStart + nextOpen.tag.length;
|
|
598
|
-
const
|
|
599
|
-
|
|
822
|
+
const valueStart = openStart + nextOpen.tag.open.length;
|
|
823
|
+
const nextValueClose = findNextCloseTag(
|
|
824
|
+
text,
|
|
825
|
+
valueStart,
|
|
826
|
+
nextOpen.tag.closes,
|
|
827
|
+
);
|
|
828
|
+
if (!nextValueClose) {
|
|
600
829
|
const value = decodeXmlEntities(text.slice(valueStart).trim());
|
|
601
830
|
if (value) thinking.push(value);
|
|
602
831
|
cursor = text.length;
|
|
603
832
|
break;
|
|
604
833
|
}
|
|
605
834
|
|
|
606
|
-
const value = decodeXmlEntities(
|
|
835
|
+
const value = decodeXmlEntities(
|
|
836
|
+
text.slice(valueStart, nextValueClose.index).trim(),
|
|
837
|
+
);
|
|
607
838
|
if (value) thinking.push(value);
|
|
608
|
-
cursor =
|
|
839
|
+
cursor = nextValueClose.index + nextValueClose.tag.length;
|
|
609
840
|
}
|
|
610
841
|
|
|
611
842
|
if (cursor === 0) {
|
|
@@ -661,9 +892,17 @@ function parseToolArguments(block: string): Record<string, unknown> {
|
|
|
661
892
|
: block.indexOf(close, openEnd + 1);
|
|
662
893
|
if (closeStart === -1 || closeStart < openEnd) break;
|
|
663
894
|
const raw = decodeXmlEntities(block.slice(openEnd + 1, closeStart).trim());
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
895
|
+
// `content` and `diff` are explicitly string parameters (file bodies,
|
|
896
|
+
// SEARCH/REPLACE diffs). Parsing them as JSON corrupts JSON file content
|
|
897
|
+
// into "[object Object]".
|
|
898
|
+
const shouldParseJson = tag !== "content" && tag !== "diff";
|
|
899
|
+
if (shouldParseJson) {
|
|
900
|
+
try {
|
|
901
|
+
args[tag] = JSON.parse(raw);
|
|
902
|
+
} catch {
|
|
903
|
+
args[tag] = raw;
|
|
904
|
+
}
|
|
905
|
+
} else {
|
|
667
906
|
args[tag] = raw;
|
|
668
907
|
}
|
|
669
908
|
cursor = closeStart + close.length;
|
|
@@ -671,20 +910,80 @@ function parseToolArguments(block: string): Record<string, unknown> {
|
|
|
671
910
|
return args;
|
|
672
911
|
}
|
|
673
912
|
|
|
913
|
+
type ParsedToolCalls = {
|
|
914
|
+
text: string;
|
|
915
|
+
toolCalls: Array<{ name: string; arguments: Record<string, unknown> }>;
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Some MiMo/Cline models emit Pi SDK `<function=name>` tool-call syntax
|
|
920
|
+
* instead of Cline XML `<toolName>` syntax:
|
|
921
|
+
*
|
|
922
|
+
* <function=read_file>
|
|
923
|
+
* <param name="path">README.md</param>
|
|
924
|
+
* </function>
|
|
925
|
+
*
|
|
926
|
+
* Parse these directly to Pi tool calls without going through Cline XML.
|
|
927
|
+
*/
|
|
928
|
+
function extractFunctionTagToolCalls(
|
|
929
|
+
text: string,
|
|
930
|
+
bridgeByRemoteName: Map<string, ToolBridge>,
|
|
931
|
+
): { text: string; toolCalls: ParsedToolCalls["toolCalls"] } {
|
|
932
|
+
const FUNCTION_TAG_RE = /<function=([a-zA-Z0-9_-]+)>([\s\S]*?)<\/function>/g;
|
|
933
|
+
const toolCalls: ParsedToolCalls["toolCalls"] = [];
|
|
934
|
+
const parts: string[] = [];
|
|
935
|
+
let cursor = 0;
|
|
936
|
+
let match: RegExpExecArray | null;
|
|
937
|
+
|
|
938
|
+
while ((match = FUNCTION_TAG_RE.exec(text)) !== null) {
|
|
939
|
+
const [fullMatch, toolName, body] = match;
|
|
940
|
+
pushTextFragment(parts, text.slice(cursor, match.index));
|
|
941
|
+
|
|
942
|
+
// Parse <param name="x">val</param> directly to arguments
|
|
943
|
+
const args: Record<string, unknown> = {};
|
|
944
|
+
const PARAM_RE = /<param\s+name="([^"]*)">([\s\S]*?)<\/param>/g;
|
|
945
|
+
let paramMatch: RegExpExecArray | null;
|
|
946
|
+
while ((paramMatch = PARAM_RE.exec(body)) !== null) {
|
|
947
|
+
args[paramMatch[1]] = paramMatch[2];
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const bridge = bridgeByRemoteName.get(toolName);
|
|
951
|
+
toolCalls.push({
|
|
952
|
+
name: bridge?.runtimeName ?? toolName,
|
|
953
|
+
arguments: bridge?.toRuntimeArgs(args) ?? args,
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
cursor = match.index + fullMatch.length;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
pushTextFragment(parts, text.slice(cursor));
|
|
960
|
+
return { text: parts.join("\n\n").trim(), toolCalls };
|
|
961
|
+
}
|
|
962
|
+
|
|
674
963
|
function parseXmlToolCalls(
|
|
675
964
|
rawText: string,
|
|
676
965
|
tools: Tool[] | undefined,
|
|
677
|
-
): {
|
|
678
|
-
|
|
679
|
-
toolCalls: Array<{ name: string; arguments: Record<string, unknown> }>;
|
|
680
|
-
} {
|
|
966
|
+
): ParsedToolCalls {
|
|
967
|
+
const bridges = getParseToolBridges(tools);
|
|
681
968
|
const bridgeByRemoteName = new Map(
|
|
682
|
-
|
|
969
|
+
bridges.map((bridge) => [bridge.remoteName, bridge]),
|
|
683
970
|
);
|
|
684
|
-
|
|
685
|
-
|
|
971
|
+
// Some Cline/MiMo variants use the Pi runtime tool name (e.g. <edit>,
|
|
972
|
+
// <write>) instead of the Cline XML name (<replace_in_file>, <write_to_file>).
|
|
973
|
+
// Register runtime names as aliases so both forms are recognised.
|
|
974
|
+
const bridgeByName = new Map(
|
|
975
|
+
bridges.flatMap((bridge) => [
|
|
976
|
+
[bridge.remoteName, bridge],
|
|
977
|
+
[bridge.runtimeName, bridge],
|
|
978
|
+
]),
|
|
979
|
+
);
|
|
980
|
+
const toolNames = new Set(bridgeByName.keys());
|
|
981
|
+
|
|
982
|
+
// Extract <function=name> Pi SDK tool calls directly (no Cline XML intermediate)
|
|
983
|
+
const fnResult = extractFunctionTagToolCalls(rawText, bridgeByRemoteName);
|
|
984
|
+
const textWithoutThinking = extractThinkingXml(fnResult.text).text;
|
|
686
985
|
if (toolNames.size === 0) {
|
|
687
|
-
return { text: textWithoutThinking.trim(), toolCalls:
|
|
986
|
+
return { text: textWithoutThinking.trim(), toolCalls: fnResult.toolCalls };
|
|
688
987
|
}
|
|
689
988
|
|
|
690
989
|
const sourceText = findNextToolStart(textWithoutThinking, toolNames, 0)
|
|
@@ -698,7 +997,9 @@ function parseXmlToolCalls(
|
|
|
698
997
|
while (cursor < sourceText.length) {
|
|
699
998
|
const next = findNextToolStart(sourceText, toolNames, cursor);
|
|
700
999
|
if (!next) break;
|
|
701
|
-
const
|
|
1000
|
+
const bridge = bridgeByName.get(next.name);
|
|
1001
|
+
const remoteName = bridge?.remoteName ?? next.name;
|
|
1002
|
+
const closeTag = `</${remoteName}>`;
|
|
702
1003
|
const closeStart = sourceText.indexOf(
|
|
703
1004
|
closeTag,
|
|
704
1005
|
next.index + next.openTag.length,
|
|
@@ -706,18 +1007,129 @@ function parseXmlToolCalls(
|
|
|
706
1007
|
pushTextFragment(textParts, sourceText.slice(cursor, next.index));
|
|
707
1008
|
const blockEnd = closeStart === -1 ? sourceText.length : closeStart;
|
|
708
1009
|
const block = sourceText.slice(next.index + next.openTag.length, blockEnd);
|
|
709
|
-
const bridge = bridgeByRemoteName.get(next.name);
|
|
710
1010
|
const remoteArgs = parseToolArguments(block);
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1011
|
+
const writeRuntimeName = getWriteRuntimeToolName(tools);
|
|
1012
|
+
const heredocWrite =
|
|
1013
|
+
remoteName === "execute_command" && writeRuntimeName
|
|
1014
|
+
? parseCatHeredocWriteCommand(stringArg(remoteArgs, "command"))
|
|
1015
|
+
: undefined;
|
|
1016
|
+
if (heredocWrite && writeRuntimeName) {
|
|
1017
|
+
toolCalls.push({
|
|
1018
|
+
name: writeRuntimeName,
|
|
1019
|
+
arguments: { ...heredocWrite },
|
|
1020
|
+
});
|
|
1021
|
+
} else {
|
|
1022
|
+
toolCalls.push({
|
|
1023
|
+
name: bridge?.runtimeName ?? next.name,
|
|
1024
|
+
arguments: bridge?.toRuntimeArgs(remoteArgs) ?? remoteArgs,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
715
1027
|
cursor =
|
|
716
1028
|
closeStart === -1 ? sourceText.length : closeStart + closeTag.length;
|
|
717
1029
|
}
|
|
718
1030
|
|
|
719
1031
|
pushTextFragment(textParts, sourceText.slice(cursor));
|
|
720
|
-
return {
|
|
1032
|
+
return {
|
|
1033
|
+
text: textParts.join("\n\n").trim(),
|
|
1034
|
+
toolCalls: [...fnResult.toolCalls, ...toolCalls],
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function parseReasoningHiddenToolCalls(
|
|
1039
|
+
thinkingParts: string[],
|
|
1040
|
+
tools: Tool[] | undefined,
|
|
1041
|
+
depth = 3,
|
|
1042
|
+
): { thinking: string[]; toolCalls: ParsedToolCalls["toolCalls"] } {
|
|
1043
|
+
const thinking: string[] = [];
|
|
1044
|
+
const toolCalls: ParsedToolCalls["toolCalls"] = [];
|
|
1045
|
+
for (const part of thinkingParts) {
|
|
1046
|
+
const trimmed = part.trim();
|
|
1047
|
+
if (!trimmed) continue;
|
|
1048
|
+
if (depth <= 0) {
|
|
1049
|
+
thinking.push(trimmed);
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
const extracted = extractThinkingXml(trimmed);
|
|
1053
|
+
const nested = parseReasoningHiddenToolCalls(
|
|
1054
|
+
extracted.thinking,
|
|
1055
|
+
tools,
|
|
1056
|
+
depth - 1,
|
|
1057
|
+
);
|
|
1058
|
+
const parsed = parseXmlToolCalls(extracted.text, tools);
|
|
1059
|
+
toolCalls.push(...parsed.toolCalls, ...nested.toolCalls);
|
|
1060
|
+
if (parsed.text) thinking.push(parsed.text);
|
|
1061
|
+
thinking.push(...nested.thinking);
|
|
1062
|
+
if (
|
|
1063
|
+
!parsed.text &&
|
|
1064
|
+
parsed.toolCalls.length === 0 &&
|
|
1065
|
+
nested.toolCalls.length === 0 &&
|
|
1066
|
+
nested.thinking.length === 0
|
|
1067
|
+
) {
|
|
1068
|
+
thinking.push(trimmed);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return { thinking, toolCalls };
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function parseReasoningToolCalls(
|
|
1075
|
+
reasoning: string,
|
|
1076
|
+
tools: Tool[] | undefined,
|
|
1077
|
+
): { thinking: string[]; toolCalls: ParsedToolCalls["toolCalls"] } {
|
|
1078
|
+
if (!reasoning.trim()) return { thinking: [], toolCalls: [] };
|
|
1079
|
+
|
|
1080
|
+
const extracted = extractThinkingXml(reasoning);
|
|
1081
|
+
const hiddenParsed = parseReasoningHiddenToolCalls(extracted.thinking, tools);
|
|
1082
|
+
const parsed = parseXmlToolCalls(extracted.text, tools);
|
|
1083
|
+
const thinking = [...hiddenParsed.thinking];
|
|
1084
|
+
if (parsed.toolCalls.length > 0 && parsed.text) {
|
|
1085
|
+
thinking.push(parsed.text);
|
|
1086
|
+
} else if (
|
|
1087
|
+
parsed.toolCalls.length === 0 &&
|
|
1088
|
+
hiddenParsed.thinking.length === 0 &&
|
|
1089
|
+
extracted.thinking.length === 0
|
|
1090
|
+
) {
|
|
1091
|
+
thinking.push(reasoning.trim());
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return {
|
|
1095
|
+
thinking,
|
|
1096
|
+
toolCalls: [...parsed.toolCalls, ...hiddenParsed.toolCalls],
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const INTERNAL_ONLY_RESPONSE =
|
|
1101
|
+
"Cline returned internal reasoning only and did not produce a user-visible response. Please retry or ask it to continue.";
|
|
1102
|
+
|
|
1103
|
+
function prepareClineXmlOutput(
|
|
1104
|
+
parsedText: string,
|
|
1105
|
+
contentThinking: string[],
|
|
1106
|
+
reasoningThinking: string[],
|
|
1107
|
+
toolCalls: ParsedToolCalls["toolCalls"],
|
|
1108
|
+
): {
|
|
1109
|
+
visibleText: string;
|
|
1110
|
+
thinkingText: string;
|
|
1111
|
+
toolCalls: ParsedToolCalls["toolCalls"];
|
|
1112
|
+
} {
|
|
1113
|
+
const thinkingParts = [...reasoningThinking, ...contentThinking].filter(
|
|
1114
|
+
Boolean,
|
|
1115
|
+
);
|
|
1116
|
+
const thinkingText = thinkingParts.join("\n\n");
|
|
1117
|
+
if (!parsedText && toolCalls.length === 0 && thinkingText) {
|
|
1118
|
+
// Never return a blank stop, but also do not surface hidden reasoning as
|
|
1119
|
+
// user-visible answer text. If Cline sends only hidden/reasoning content,
|
|
1120
|
+
// show a stable visible fallback and keep the raw content in thinking.
|
|
1121
|
+
return {
|
|
1122
|
+
visibleText: INTERNAL_ONLY_RESPONSE,
|
|
1123
|
+
thinkingText,
|
|
1124
|
+
toolCalls,
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return {
|
|
1129
|
+
visibleText: parsedText,
|
|
1130
|
+
thinkingText,
|
|
1131
|
+
toolCalls,
|
|
1132
|
+
};
|
|
721
1133
|
}
|
|
722
1134
|
|
|
723
1135
|
function usageFromChunkUsage(usage: ClineXmlChunk["usage"] | undefined): Usage {
|
|
@@ -754,6 +1166,121 @@ async function* parseSse(response: Response): AsyncGenerator<ClineXmlChunk> {
|
|
|
754
1166
|
}
|
|
755
1167
|
}
|
|
756
1168
|
|
|
1169
|
+
type ClineXmlResponseData = {
|
|
1170
|
+
rawText: string;
|
|
1171
|
+
thinking: string;
|
|
1172
|
+
finishReason: string | null | undefined;
|
|
1173
|
+
usage: ClineXmlChunk["usage"] | undefined;
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
function isRetryableClineReasoningStreamError(error: unknown): boolean {
|
|
1177
|
+
if (!(error instanceof Error)) return false;
|
|
1178
|
+
const message = error.message.toLowerCase();
|
|
1179
|
+
return message.includes("stream error occurred");
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
async function readClineXmlResponse(
|
|
1183
|
+
response: Response,
|
|
1184
|
+
): Promise<ClineXmlResponseData> {
|
|
1185
|
+
let rawText = "";
|
|
1186
|
+
let thinking = "";
|
|
1187
|
+
let finishReason: string | null | undefined;
|
|
1188
|
+
let usage: ClineXmlChunk["usage"] | undefined;
|
|
1189
|
+
|
|
1190
|
+
for await (const chunk of parseSse(response)) {
|
|
1191
|
+
if (chunk.error) {
|
|
1192
|
+
throw new Error(
|
|
1193
|
+
`${chunk.error.code ?? "cline_error"}: ${chunk.error.message ?? "Unknown Cline error"}`,
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
if (chunk.usage) usage = chunk.usage;
|
|
1197
|
+
const choice = chunk.choices?.[0];
|
|
1198
|
+
if (!choice) continue;
|
|
1199
|
+
if (choice.error) {
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
`${choice.error.code ?? "cline_error"}: ${choice.error.message ?? "Unknown Cline error"}`,
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
1205
|
+
rawText += choice.delta?.content ?? "";
|
|
1206
|
+
thinking += choice.delta?.reasoning ?? "";
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (!rawText.trim() && !thinking.trim()) {
|
|
1210
|
+
throw new Error("Cline returned empty response");
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Some MiMo/Cline models wrap XML tags in Unicode math-italic characters
|
|
1214
|
+
// forming "anthml:" prefixes (e.g. <𝑎𝑛𝑡𝑚𝑙:thinking>, <𝑎𝑛𝑡𝑚𝑙:read_file>).
|
|
1215
|
+
// Strip these so the rest of the parser sees standard ASCII XML tags.
|
|
1216
|
+
return {
|
|
1217
|
+
rawText: normalizeDecoratedXmlTags(rawText),
|
|
1218
|
+
thinking: normalizeDecoratedXmlTags(thinking),
|
|
1219
|
+
finishReason,
|
|
1220
|
+
usage,
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
async function fetchClineXmlResponse(
|
|
1225
|
+
model: Model<string>,
|
|
1226
|
+
context: Context,
|
|
1227
|
+
options: SimpleStreamOptions,
|
|
1228
|
+
headers: Record<string, string>,
|
|
1229
|
+
includeReasoning: boolean,
|
|
1230
|
+
): Promise<ClineXmlResponseData> {
|
|
1231
|
+
const response = await fetch(`${BASE_URL_CLINE}/chat/completions`, {
|
|
1232
|
+
method: "POST",
|
|
1233
|
+
headers: {
|
|
1234
|
+
...headers,
|
|
1235
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
1236
|
+
"Content-Type": "application/json",
|
|
1237
|
+
},
|
|
1238
|
+
body: JSON.stringify({
|
|
1239
|
+
model: normalizeApiModelId(model.id),
|
|
1240
|
+
temperature: 0,
|
|
1241
|
+
messages: buildClineXmlMessages(context),
|
|
1242
|
+
stream: true,
|
|
1243
|
+
stream_options: { include_usage: true },
|
|
1244
|
+
...(includeReasoning ? { include_reasoning: true } : {}),
|
|
1245
|
+
}),
|
|
1246
|
+
signal: options.signal,
|
|
1247
|
+
});
|
|
1248
|
+
await options.onResponse?.(
|
|
1249
|
+
{
|
|
1250
|
+
status: response.status,
|
|
1251
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
1252
|
+
},
|
|
1253
|
+
model,
|
|
1254
|
+
);
|
|
1255
|
+
|
|
1256
|
+
if (!response.ok) {
|
|
1257
|
+
throw new Error(
|
|
1258
|
+
`Cline API error ${response.status}: ${await response.text()}`,
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return readClineXmlResponse(response);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
async function fetchClineXmlResponseWithReasoningFallback(
|
|
1266
|
+
model: Model<string>,
|
|
1267
|
+
context: Context,
|
|
1268
|
+
options: SimpleStreamOptions,
|
|
1269
|
+
headers: Record<string, string>,
|
|
1270
|
+
): Promise<ClineXmlResponseData> {
|
|
1271
|
+
try {
|
|
1272
|
+
return await fetchClineXmlResponse(model, context, options, headers, true);
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
if (
|
|
1275
|
+
options.signal?.aborted ||
|
|
1276
|
+
!isRetryableClineReasoningStreamError(error)
|
|
1277
|
+
) {
|
|
1278
|
+
throw error;
|
|
1279
|
+
}
|
|
1280
|
+
return fetchClineXmlResponse(model, context, options, headers, false);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
757
1284
|
function createAssistant(model: Model<string>): AssistantMessage {
|
|
758
1285
|
return {
|
|
759
1286
|
role: "assistant",
|
|
@@ -871,78 +1398,71 @@ export function streamClineXml(
|
|
|
871
1398
|
throw new Error("No Cline access token found. Run /login cline first.");
|
|
872
1399
|
}
|
|
873
1400
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
...headers,
|
|
878
|
-
Authorization: `Bearer ${options.apiKey}`,
|
|
879
|
-
"Content-Type": "application/json",
|
|
880
|
-
},
|
|
881
|
-
body: JSON.stringify({
|
|
882
|
-
model: normalizeApiModelId(model.id),
|
|
883
|
-
temperature: 0,
|
|
884
|
-
messages: buildClineXmlMessages(context),
|
|
885
|
-
stream: true,
|
|
886
|
-
stream_options: { include_usage: true },
|
|
887
|
-
include_reasoning: true,
|
|
888
|
-
}),
|
|
889
|
-
signal: options.signal,
|
|
890
|
-
});
|
|
891
|
-
await options.onResponse?.(
|
|
892
|
-
{
|
|
893
|
-
status: response.status,
|
|
894
|
-
headers: Object.fromEntries(response.headers.entries()),
|
|
895
|
-
},
|
|
896
|
-
model,
|
|
897
|
-
);
|
|
898
|
-
|
|
899
|
-
if (!response.ok) {
|
|
900
|
-
throw new Error(
|
|
901
|
-
`Cline API error ${response.status}: ${await response.text()}`,
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
let rawText = "";
|
|
906
|
-
let thinking = "";
|
|
1401
|
+
let output: ReturnType<typeof prepareClineXmlOutput>;
|
|
1402
|
+
let rawText: string;
|
|
1403
|
+
let thinking: string;
|
|
907
1404
|
let finishReason: string | null | undefined;
|
|
908
1405
|
let usage: ClineXmlChunk["usage"] | undefined;
|
|
1406
|
+
let currentContext = context;
|
|
1407
|
+
|
|
1408
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1409
|
+
const data = await fetchClineXmlResponseWithReasoningFallback(
|
|
1410
|
+
model,
|
|
1411
|
+
currentContext,
|
|
1412
|
+
options,
|
|
1413
|
+
headers,
|
|
1414
|
+
);
|
|
1415
|
+
rawText = data.rawText;
|
|
1416
|
+
thinking = data.thinking;
|
|
1417
|
+
finishReason = data.finishReason;
|
|
1418
|
+
usage = data.usage;
|
|
1419
|
+
|
|
1420
|
+
const extractedThinking = extractThinkingXml(rawText);
|
|
1421
|
+
const parsedReasoning = parseReasoningToolCalls(
|
|
1422
|
+
thinking,
|
|
1423
|
+
currentContext.tools,
|
|
1424
|
+
);
|
|
1425
|
+
const parsed = parseXmlToolCalls(
|
|
1426
|
+
extractedThinking.text,
|
|
1427
|
+
currentContext.tools,
|
|
1428
|
+
);
|
|
1429
|
+
output = prepareClineXmlOutput(
|
|
1430
|
+
parsed.text,
|
|
1431
|
+
extractedThinking.thinking,
|
|
1432
|
+
parsedReasoning.thinking,
|
|
1433
|
+
[...parsed.toolCalls, ...parsedReasoning.toolCalls],
|
|
1434
|
+
);
|
|
909
1435
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1436
|
+
// Reasoning-only response: MiMo stopped without producing visible
|
|
1437
|
+
// text or tool calls. Auto-retry once with a "continue" nudge
|
|
1438
|
+
// instead of showing a dead-end error to the user.
|
|
1439
|
+
if (output.visibleText === INTERNAL_ONLY_RESPONSE && attempt === 0) {
|
|
1440
|
+
currentContext = {
|
|
1441
|
+
...context,
|
|
1442
|
+
messages: [
|
|
1443
|
+
...context.messages,
|
|
1444
|
+
{
|
|
1445
|
+
role: "user" as const,
|
|
1446
|
+
content: [{ type: "text" as const, text: "Please continue." }],
|
|
1447
|
+
timestamp: Date.now(),
|
|
1448
|
+
},
|
|
1449
|
+
],
|
|
1450
|
+
};
|
|
1451
|
+
continue;
|
|
923
1452
|
}
|
|
924
|
-
|
|
925
|
-
rawText += choice.delta?.content ?? "";
|
|
926
|
-
thinking += choice.delta?.reasoning ?? "";
|
|
1453
|
+
break;
|
|
927
1454
|
}
|
|
928
1455
|
|
|
929
|
-
assistant.usage = usageFromChunkUsage(usage);
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
.filter(Boolean)
|
|
935
|
-
.join("\n\n"),
|
|
936
|
-
stream,
|
|
937
|
-
);
|
|
938
|
-
const parsed = parseXmlToolCalls(extractedThinking.text, context.tools);
|
|
939
|
-
pushText(assistant, parsed.text, stream);
|
|
940
|
-
for (const toolCall of parsed.toolCalls) {
|
|
1456
|
+
assistant.usage = usageFromChunkUsage(usage!);
|
|
1457
|
+
pushThinking(assistant, output!.thinkingText, stream);
|
|
1458
|
+
pushText(assistant, output!.visibleText, stream);
|
|
1459
|
+
const toolCalls = output!.toolCalls;
|
|
1460
|
+
for (const toolCall of toolCalls) {
|
|
941
1461
|
pushToolCall(assistant, toolCall, stream);
|
|
942
1462
|
}
|
|
943
1463
|
|
|
944
1464
|
assistant.stopReason =
|
|
945
|
-
|
|
1465
|
+
toolCalls.length > 0
|
|
946
1466
|
? "toolUse"
|
|
947
1467
|
: finishReason === "length"
|
|
948
1468
|
? "length"
|
|
@@ -969,6 +1489,12 @@ export function streamClineXml(
|
|
|
969
1489
|
|
|
970
1490
|
export const __test__ = {
|
|
971
1491
|
buildClineXmlMessages,
|
|
1492
|
+
extractFunctionTagToolCalls,
|
|
1493
|
+
isRetryableClineReasoningStreamError,
|
|
1494
|
+
normalizeDecoratedXmlTags,
|
|
1495
|
+
parseReasoningHiddenToolCalls,
|
|
1496
|
+
parseReasoningToolCalls,
|
|
972
1497
|
parseXmlToolCalls,
|
|
1498
|
+
prepareClineXmlOutput,
|
|
973
1499
|
serializeXmlToolCall,
|
|
974
1500
|
};
|