pi-free 2.1.0 → 2.1.1
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 +26 -3
- package/lib/built-in-toggle.ts +0 -40
- package/package.json +1 -1
- package/provider-helper.ts +1 -25
- package/providers/cline/cline-xml-bridge.ts +594 -97
- package/providers/cline/cline.ts +0 -23
- package/providers/codestral/codestral.ts +0 -11
- package/providers/dynamic-built-in/index.ts +0 -20
- package/providers/kilo/kilo.ts +2 -19
- package/providers/ollama/ollama.ts +0 -13
- 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("&", "&")
|
|
@@ -291,6 +358,111 @@ function executeCommandBridge(tool?: Tool): ToolBridge {
|
|
|
291
358
|
};
|
|
292
359
|
}
|
|
293
360
|
|
|
361
|
+
type HeredocWriteCommand = {
|
|
362
|
+
path: string;
|
|
363
|
+
content: string;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
function shellSplitLine(line: string): string[] {
|
|
367
|
+
const tokens: string[] = [];
|
|
368
|
+
let current = "";
|
|
369
|
+
let quote: '"' | "'" | undefined;
|
|
370
|
+
|
|
371
|
+
for (let i = 0; i < line.length; i++) {
|
|
372
|
+
const char = line[i];
|
|
373
|
+
if (quote) {
|
|
374
|
+
if (char === quote) {
|
|
375
|
+
quote = undefined;
|
|
376
|
+
} else {
|
|
377
|
+
current += char;
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (char === '"' || char === "'") {
|
|
382
|
+
quote = char;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (char === " " || char === "\t") {
|
|
386
|
+
if (current) {
|
|
387
|
+
tokens.push(current);
|
|
388
|
+
current = "";
|
|
389
|
+
}
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
current += char;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (current) tokens.push(current);
|
|
396
|
+
return tokens;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function parseCatHeredocWriteCommand(
|
|
400
|
+
command: string,
|
|
401
|
+
): HeredocWriteCommand | undefined {
|
|
402
|
+
const normalized = command.replaceAll("\r\n", "\n").trim();
|
|
403
|
+
const lines = normalized.split("\n");
|
|
404
|
+
if (lines.length < 3) return undefined;
|
|
405
|
+
|
|
406
|
+
const tokens = shellSplitLine(lines[0].trim());
|
|
407
|
+
if (tokens[0] !== "cat") return undefined;
|
|
408
|
+
const redirectIndex = tokens.indexOf(">");
|
|
409
|
+
if (redirectIndex === -1) return undefined;
|
|
410
|
+
const path = tokens[redirectIndex + 1];
|
|
411
|
+
if (!path) return undefined;
|
|
412
|
+
|
|
413
|
+
let delimiter = "";
|
|
414
|
+
for (let i = redirectIndex + 2; i < tokens.length; i++) {
|
|
415
|
+
const token = tokens[i];
|
|
416
|
+
if (token === "<<") {
|
|
417
|
+
delimiter = tokens[i + 1] ?? "";
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
if (token.startsWith("<<")) {
|
|
421
|
+
delimiter = token.slice(2);
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (!delimiter) return undefined;
|
|
426
|
+
|
|
427
|
+
let delimiterLine = -1;
|
|
428
|
+
for (let i = 1; i < lines.length; i++) {
|
|
429
|
+
if (lines[i].trim() === delimiter) {
|
|
430
|
+
delimiterLine = i;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (delimiterLine === -1) return undefined;
|
|
435
|
+
|
|
436
|
+
const trailing = lines
|
|
437
|
+
.slice(delimiterLine + 1)
|
|
438
|
+
.join("\n")
|
|
439
|
+
.trim();
|
|
440
|
+
if (trailing) {
|
|
441
|
+
const trailingLines = trailing.split("\n").filter((line) => line.trim());
|
|
442
|
+
if (trailingLines.length !== 1) return undefined;
|
|
443
|
+
const trailingTokens = shellSplitLine(trailingLines[0].trim());
|
|
444
|
+
if (trailingTokens.length !== 2 || trailingTokens[0] !== "cat") {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
if (trailingTokens[1] !== path) return undefined;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
path,
|
|
452
|
+
content: lines.slice(1, delimiterLine).join("\n"),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function getWriteRuntimeToolName(
|
|
457
|
+
tools: Tool[] | undefined,
|
|
458
|
+
): string | undefined {
|
|
459
|
+
if ((tools ?? []).some((tool) => tool.name === "write_to_file")) {
|
|
460
|
+
return "write_to_file";
|
|
461
|
+
}
|
|
462
|
+
if ((tools ?? []).some((tool) => tool.name === "write")) return "write";
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
465
|
+
|
|
294
466
|
function listFilesBridge(): ToolBridge {
|
|
295
467
|
return {
|
|
296
468
|
remoteName: "list_files",
|
|
@@ -558,54 +730,98 @@ function pushTextFragment(textParts: string[], fragment: string): void {
|
|
|
558
730
|
textParts.push(trimmed);
|
|
559
731
|
}
|
|
560
732
|
|
|
733
|
+
type HiddenThoughtTag = {
|
|
734
|
+
open: string;
|
|
735
|
+
closes: string[];
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const HIDDEN_THOUGHT_TAGS: HiddenThoughtTag[] = [
|
|
739
|
+
{ open: "<thinking>", closes: ["</thinking>"] },
|
|
740
|
+
// Some DeepSeek/Cline variants open with <think> but close with </thinking>.
|
|
741
|
+
{ open: "<think>", closes: ["</think>", "</thinking>"] },
|
|
742
|
+
// Compaction/summary artifacts can leak into Cline content as </summary>.
|
|
743
|
+
{ open: "<summary>", closes: ["</summary>"] },
|
|
744
|
+
// Cline may emit persistent issue-checking as hidden deliberation.
|
|
745
|
+
{
|
|
746
|
+
open: "<persistent_issue_checking>",
|
|
747
|
+
closes: ["</persistent_issue_checking>"],
|
|
748
|
+
},
|
|
749
|
+
];
|
|
750
|
+
|
|
751
|
+
const HIDDEN_THOUGHT_CLOSE_TAGS = Array.from(
|
|
752
|
+
new Set(HIDDEN_THOUGHT_TAGS.flatMap((tag) => tag.closes)),
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
function findNextHiddenOpenTag(
|
|
756
|
+
text: string,
|
|
757
|
+
from: number,
|
|
758
|
+
): { index: number; tag: HiddenThoughtTag } | null {
|
|
759
|
+
let best: { index: number; tag: HiddenThoughtTag } | null = null;
|
|
760
|
+
for (const tag of HIDDEN_THOUGHT_TAGS) {
|
|
761
|
+
const index = text.indexOf(tag.open, from);
|
|
762
|
+
if (index === -1) continue;
|
|
763
|
+
if (!best || index < best.index) best = { index, tag };
|
|
764
|
+
}
|
|
765
|
+
return best;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function findNextCloseTag(
|
|
769
|
+
text: string,
|
|
770
|
+
from: number,
|
|
771
|
+
closeTags: string[],
|
|
772
|
+
): { index: number; tag: string } | null {
|
|
773
|
+
let best: { index: number; tag: string } | null = null;
|
|
774
|
+
for (const tag of closeTags) {
|
|
775
|
+
const index = text.indexOf(tag, from);
|
|
776
|
+
if (index === -1) continue;
|
|
777
|
+
if (!best || index < best.index) best = { index, tag };
|
|
778
|
+
}
|
|
779
|
+
return best;
|
|
780
|
+
}
|
|
781
|
+
|
|
561
782
|
function extractThinkingXml(text: string): {
|
|
562
783
|
text: string;
|
|
563
784
|
thinking: string[];
|
|
564
785
|
} {
|
|
565
786
|
const thinking: string[] = [];
|
|
566
787
|
const parts: string[] = [];
|
|
567
|
-
const openTags = ["<thinking>", "<think>"];
|
|
568
|
-
const closeTag = "</thinking>";
|
|
569
788
|
let cursor = 0;
|
|
570
789
|
|
|
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
790
|
while (cursor < text.length) {
|
|
582
|
-
const nextOpen =
|
|
791
|
+
const nextOpen = findNextHiddenOpenTag(text, cursor);
|
|
583
792
|
const openStart = nextOpen?.index ?? -1;
|
|
584
|
-
const
|
|
793
|
+
const nextClose = findNextCloseTag(text, cursor, HIDDEN_THOUGHT_CLOSE_TAGS);
|
|
794
|
+
const closeStart = nextClose?.index ?? -1;
|
|
585
795
|
|
|
586
|
-
if (
|
|
796
|
+
if (nextClose && (openStart === -1 || closeStart < openStart)) {
|
|
587
797
|
const danglingThinking = decodeXmlEntities(
|
|
588
798
|
text.slice(cursor, closeStart).trim(),
|
|
589
799
|
);
|
|
590
800
|
if (danglingThinking) thinking.push(danglingThinking);
|
|
591
|
-
cursor = closeStart +
|
|
801
|
+
cursor = closeStart + nextClose.tag.length;
|
|
592
802
|
continue;
|
|
593
803
|
}
|
|
594
804
|
|
|
595
805
|
if (openStart === -1 || !nextOpen) break;
|
|
596
806
|
parts.push(text.slice(cursor, openStart));
|
|
597
|
-
const valueStart = openStart + nextOpen.tag.length;
|
|
598
|
-
const
|
|
599
|
-
|
|
807
|
+
const valueStart = openStart + nextOpen.tag.open.length;
|
|
808
|
+
const nextValueClose = findNextCloseTag(
|
|
809
|
+
text,
|
|
810
|
+
valueStart,
|
|
811
|
+
nextOpen.tag.closes,
|
|
812
|
+
);
|
|
813
|
+
if (!nextValueClose) {
|
|
600
814
|
const value = decodeXmlEntities(text.slice(valueStart).trim());
|
|
601
815
|
if (value) thinking.push(value);
|
|
602
816
|
cursor = text.length;
|
|
603
817
|
break;
|
|
604
818
|
}
|
|
605
819
|
|
|
606
|
-
const value = decodeXmlEntities(
|
|
820
|
+
const value = decodeXmlEntities(
|
|
821
|
+
text.slice(valueStart, nextValueClose.index).trim(),
|
|
822
|
+
);
|
|
607
823
|
if (value) thinking.push(value);
|
|
608
|
-
cursor =
|
|
824
|
+
cursor = nextValueClose.index + nextValueClose.tag.length;
|
|
609
825
|
}
|
|
610
826
|
|
|
611
827
|
if (cursor === 0) {
|
|
@@ -661,9 +877,17 @@ function parseToolArguments(block: string): Record<string, unknown> {
|
|
|
661
877
|
: block.indexOf(close, openEnd + 1);
|
|
662
878
|
if (closeStart === -1 || closeStart < openEnd) break;
|
|
663
879
|
const raw = decodeXmlEntities(block.slice(openEnd + 1, closeStart).trim());
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
880
|
+
// `content` and `diff` are explicitly string parameters (file bodies,
|
|
881
|
+
// SEARCH/REPLACE diffs). Parsing them as JSON corrupts JSON file content
|
|
882
|
+
// into "[object Object]".
|
|
883
|
+
const shouldParseJson = tag !== "content" && tag !== "diff";
|
|
884
|
+
if (shouldParseJson) {
|
|
885
|
+
try {
|
|
886
|
+
args[tag] = JSON.parse(raw);
|
|
887
|
+
} catch {
|
|
888
|
+
args[tag] = raw;
|
|
889
|
+
}
|
|
890
|
+
} else {
|
|
667
891
|
args[tag] = raw;
|
|
668
892
|
}
|
|
669
893
|
cursor = closeStart + close.length;
|
|
@@ -671,20 +895,70 @@ function parseToolArguments(block: string): Record<string, unknown> {
|
|
|
671
895
|
return args;
|
|
672
896
|
}
|
|
673
897
|
|
|
898
|
+
type ParsedToolCalls = {
|
|
899
|
+
text: string;
|
|
900
|
+
toolCalls: Array<{ name: string; arguments: Record<string, unknown> }>;
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Some MiMo/Cline models emit Pi SDK `<function=name>` tool-call syntax
|
|
905
|
+
* instead of Cline XML `<toolName>` syntax:
|
|
906
|
+
*
|
|
907
|
+
* <function=read_file>
|
|
908
|
+
* <param name="path">README.md</param>
|
|
909
|
+
* </function>
|
|
910
|
+
*
|
|
911
|
+
* Parse these directly to Pi tool calls without going through Cline XML.
|
|
912
|
+
*/
|
|
913
|
+
function extractFunctionTagToolCalls(
|
|
914
|
+
text: string,
|
|
915
|
+
bridgeByRemoteName: Map<string, ToolBridge>,
|
|
916
|
+
): { text: string; toolCalls: ParsedToolCalls["toolCalls"] } {
|
|
917
|
+
const FUNCTION_TAG_RE = /<function=([a-zA-Z0-9_-]+)>([\s\S]*?)<\/function>/g;
|
|
918
|
+
const toolCalls: ParsedToolCalls["toolCalls"] = [];
|
|
919
|
+
const parts: string[] = [];
|
|
920
|
+
let cursor = 0;
|
|
921
|
+
let match: RegExpExecArray | null;
|
|
922
|
+
|
|
923
|
+
while ((match = FUNCTION_TAG_RE.exec(text)) !== null) {
|
|
924
|
+
const [fullMatch, toolName, body] = match;
|
|
925
|
+
pushTextFragment(parts, text.slice(cursor, match.index));
|
|
926
|
+
|
|
927
|
+
// Parse <param name="x">val</param> directly to arguments
|
|
928
|
+
const args: Record<string, unknown> = {};
|
|
929
|
+
const PARAM_RE = /<param\s+name="([^"]*)">([\s\S]*?)<\/param>/g;
|
|
930
|
+
let paramMatch: RegExpExecArray | null;
|
|
931
|
+
while ((paramMatch = PARAM_RE.exec(body)) !== null) {
|
|
932
|
+
args[paramMatch[1]] = paramMatch[2];
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const bridge = bridgeByRemoteName.get(toolName);
|
|
936
|
+
toolCalls.push({
|
|
937
|
+
name: bridge?.runtimeName ?? toolName,
|
|
938
|
+
arguments: bridge?.toRuntimeArgs(args) ?? args,
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
cursor = match.index + fullMatch.length;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
pushTextFragment(parts, text.slice(cursor));
|
|
945
|
+
return { text: parts.join("\n\n").trim(), toolCalls };
|
|
946
|
+
}
|
|
947
|
+
|
|
674
948
|
function parseXmlToolCalls(
|
|
675
949
|
rawText: string,
|
|
676
950
|
tools: Tool[] | undefined,
|
|
677
|
-
): {
|
|
678
|
-
text: string;
|
|
679
|
-
toolCalls: Array<{ name: string; arguments: Record<string, unknown> }>;
|
|
680
|
-
} {
|
|
951
|
+
): ParsedToolCalls {
|
|
681
952
|
const bridgeByRemoteName = new Map(
|
|
682
953
|
getParseToolBridges(tools).map((bridge) => [bridge.remoteName, bridge]),
|
|
683
954
|
);
|
|
684
955
|
const toolNames = new Set(bridgeByRemoteName.keys());
|
|
685
|
-
|
|
956
|
+
|
|
957
|
+
// Extract <function=name> Pi SDK tool calls directly (no Cline XML intermediate)
|
|
958
|
+
const fnResult = extractFunctionTagToolCalls(rawText, bridgeByRemoteName);
|
|
959
|
+
const textWithoutThinking = extractThinkingXml(fnResult.text).text;
|
|
686
960
|
if (toolNames.size === 0) {
|
|
687
|
-
return { text: textWithoutThinking.trim(), toolCalls:
|
|
961
|
+
return { text: textWithoutThinking.trim(), toolCalls: fnResult.toolCalls };
|
|
688
962
|
}
|
|
689
963
|
|
|
690
964
|
const sourceText = findNextToolStart(textWithoutThinking, toolNames, 0)
|
|
@@ -708,16 +982,125 @@ function parseXmlToolCalls(
|
|
|
708
982
|
const block = sourceText.slice(next.index + next.openTag.length, blockEnd);
|
|
709
983
|
const bridge = bridgeByRemoteName.get(next.name);
|
|
710
984
|
const remoteArgs = parseToolArguments(block);
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
985
|
+
const writeRuntimeName = getWriteRuntimeToolName(tools);
|
|
986
|
+
const heredocWrite =
|
|
987
|
+
next.name === "execute_command" && writeRuntimeName
|
|
988
|
+
? parseCatHeredocWriteCommand(stringArg(remoteArgs, "command"))
|
|
989
|
+
: undefined;
|
|
990
|
+
if (heredocWrite && writeRuntimeName) {
|
|
991
|
+
toolCalls.push({
|
|
992
|
+
name: writeRuntimeName,
|
|
993
|
+
arguments: { ...heredocWrite },
|
|
994
|
+
});
|
|
995
|
+
} else {
|
|
996
|
+
toolCalls.push({
|
|
997
|
+
name: bridge?.runtimeName ?? next.name,
|
|
998
|
+
arguments: bridge?.toRuntimeArgs(remoteArgs) ?? remoteArgs,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
715
1001
|
cursor =
|
|
716
1002
|
closeStart === -1 ? sourceText.length : closeStart + closeTag.length;
|
|
717
1003
|
}
|
|
718
1004
|
|
|
719
1005
|
pushTextFragment(textParts, sourceText.slice(cursor));
|
|
720
|
-
return { text: textParts.join("\n\n").trim(), toolCalls };
|
|
1006
|
+
return { text: textParts.join("\n\n").trim(), toolCalls: [...fnResult.toolCalls, ...toolCalls] };
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function parseReasoningHiddenToolCalls(
|
|
1010
|
+
thinkingParts: string[],
|
|
1011
|
+
tools: Tool[] | undefined,
|
|
1012
|
+
depth = 3,
|
|
1013
|
+
): { thinking: string[]; toolCalls: ParsedToolCalls["toolCalls"] } {
|
|
1014
|
+
const thinking: string[] = [];
|
|
1015
|
+
const toolCalls: ParsedToolCalls["toolCalls"] = [];
|
|
1016
|
+
for (const part of thinkingParts) {
|
|
1017
|
+
const trimmed = part.trim();
|
|
1018
|
+
if (!trimmed) continue;
|
|
1019
|
+
if (depth <= 0) {
|
|
1020
|
+
thinking.push(trimmed);
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
const extracted = extractThinkingXml(trimmed);
|
|
1024
|
+
const nested = parseReasoningHiddenToolCalls(
|
|
1025
|
+
extracted.thinking,
|
|
1026
|
+
tools,
|
|
1027
|
+
depth - 1,
|
|
1028
|
+
);
|
|
1029
|
+
const parsed = parseXmlToolCalls(extracted.text, tools);
|
|
1030
|
+
toolCalls.push(...parsed.toolCalls, ...nested.toolCalls);
|
|
1031
|
+
if (parsed.text) thinking.push(parsed.text);
|
|
1032
|
+
thinking.push(...nested.thinking);
|
|
1033
|
+
if (
|
|
1034
|
+
!parsed.text &&
|
|
1035
|
+
parsed.toolCalls.length === 0 &&
|
|
1036
|
+
nested.toolCalls.length === 0 &&
|
|
1037
|
+
nested.thinking.length === 0
|
|
1038
|
+
) {
|
|
1039
|
+
thinking.push(trimmed);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return { thinking, toolCalls };
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function parseReasoningToolCalls(
|
|
1046
|
+
reasoning: string,
|
|
1047
|
+
tools: Tool[] | undefined,
|
|
1048
|
+
): { thinking: string[]; toolCalls: ParsedToolCalls["toolCalls"] } {
|
|
1049
|
+
if (!reasoning.trim()) return { thinking: [], toolCalls: [] };
|
|
1050
|
+
|
|
1051
|
+
const extracted = extractThinkingXml(reasoning);
|
|
1052
|
+
const hiddenParsed = parseReasoningHiddenToolCalls(extracted.thinking, tools);
|
|
1053
|
+
const parsed = parseXmlToolCalls(extracted.text, tools);
|
|
1054
|
+
const thinking = [...hiddenParsed.thinking];
|
|
1055
|
+
if (parsed.toolCalls.length > 0 && parsed.text) {
|
|
1056
|
+
thinking.push(parsed.text);
|
|
1057
|
+
} else if (
|
|
1058
|
+
parsed.toolCalls.length === 0 &&
|
|
1059
|
+
hiddenParsed.thinking.length === 0 &&
|
|
1060
|
+
extracted.thinking.length === 0
|
|
1061
|
+
) {
|
|
1062
|
+
thinking.push(reasoning.trim());
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return {
|
|
1066
|
+
thinking,
|
|
1067
|
+
toolCalls: [...parsed.toolCalls, ...hiddenParsed.toolCalls],
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const INTERNAL_ONLY_RESPONSE =
|
|
1072
|
+
"Cline returned internal reasoning only and did not produce a user-visible response. Please retry or ask it to continue.";
|
|
1073
|
+
|
|
1074
|
+
function prepareClineXmlOutput(
|
|
1075
|
+
parsedText: string,
|
|
1076
|
+
contentThinking: string[],
|
|
1077
|
+
reasoningThinking: string[],
|
|
1078
|
+
toolCalls: ParsedToolCalls["toolCalls"],
|
|
1079
|
+
): {
|
|
1080
|
+
visibleText: string;
|
|
1081
|
+
thinkingText: string;
|
|
1082
|
+
toolCalls: ParsedToolCalls["toolCalls"];
|
|
1083
|
+
} {
|
|
1084
|
+
const thinkingParts = [...reasoningThinking, ...contentThinking].filter(
|
|
1085
|
+
Boolean,
|
|
1086
|
+
);
|
|
1087
|
+
const thinkingText = thinkingParts.join("\n\n");
|
|
1088
|
+
if (!parsedText && toolCalls.length === 0 && thinkingText) {
|
|
1089
|
+
// Never return a blank stop, but also do not surface hidden reasoning as
|
|
1090
|
+
// user-visible answer text. If Cline sends only hidden/reasoning content,
|
|
1091
|
+
// show a stable visible fallback and keep the raw content in thinking.
|
|
1092
|
+
return {
|
|
1093
|
+
visibleText: INTERNAL_ONLY_RESPONSE,
|
|
1094
|
+
thinkingText,
|
|
1095
|
+
toolCalls,
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
return {
|
|
1100
|
+
visibleText: parsedText,
|
|
1101
|
+
thinkingText,
|
|
1102
|
+
toolCalls,
|
|
1103
|
+
};
|
|
721
1104
|
}
|
|
722
1105
|
|
|
723
1106
|
function usageFromChunkUsage(usage: ClineXmlChunk["usage"] | undefined): Usage {
|
|
@@ -754,6 +1137,121 @@ async function* parseSse(response: Response): AsyncGenerator<ClineXmlChunk> {
|
|
|
754
1137
|
}
|
|
755
1138
|
}
|
|
756
1139
|
|
|
1140
|
+
type ClineXmlResponseData = {
|
|
1141
|
+
rawText: string;
|
|
1142
|
+
thinking: string;
|
|
1143
|
+
finishReason: string | null | undefined;
|
|
1144
|
+
usage: ClineXmlChunk["usage"] | undefined;
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
function isRetryableClineReasoningStreamError(error: unknown): boolean {
|
|
1148
|
+
if (!(error instanceof Error)) return false;
|
|
1149
|
+
const message = error.message.toLowerCase();
|
|
1150
|
+
return message.includes("stream error occurred");
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async function readClineXmlResponse(
|
|
1154
|
+
response: Response,
|
|
1155
|
+
): Promise<ClineXmlResponseData> {
|
|
1156
|
+
let rawText = "";
|
|
1157
|
+
let thinking = "";
|
|
1158
|
+
let finishReason: string | null | undefined;
|
|
1159
|
+
let usage: ClineXmlChunk["usage"] | undefined;
|
|
1160
|
+
|
|
1161
|
+
for await (const chunk of parseSse(response)) {
|
|
1162
|
+
if (chunk.error) {
|
|
1163
|
+
throw new Error(
|
|
1164
|
+
`${chunk.error.code ?? "cline_error"}: ${chunk.error.message ?? "Unknown Cline error"}`,
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
if (chunk.usage) usage = chunk.usage;
|
|
1168
|
+
const choice = chunk.choices?.[0];
|
|
1169
|
+
if (!choice) continue;
|
|
1170
|
+
if (choice.error) {
|
|
1171
|
+
throw new Error(
|
|
1172
|
+
`${choice.error.code ?? "cline_error"}: ${choice.error.message ?? "Unknown Cline error"}`,
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
1176
|
+
rawText += choice.delta?.content ?? "";
|
|
1177
|
+
thinking += choice.delta?.reasoning ?? "";
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (!rawText.trim() && !thinking.trim()) {
|
|
1181
|
+
throw new Error("Cline returned empty response");
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Some MiMo/Cline models wrap XML tags in Unicode math-italic characters
|
|
1185
|
+
// forming "anthml:" prefixes (e.g. <𝑎𝑛𝑡𝑚𝑙:thinking>, <𝑎𝑛𝑡𝑚𝑙:read_file>).
|
|
1186
|
+
// Strip these so the rest of the parser sees standard ASCII XML tags.
|
|
1187
|
+
return {
|
|
1188
|
+
rawText: normalizeDecoratedXmlTags(rawText),
|
|
1189
|
+
thinking: normalizeDecoratedXmlTags(thinking),
|
|
1190
|
+
finishReason,
|
|
1191
|
+
usage,
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
async function fetchClineXmlResponse(
|
|
1196
|
+
model: Model<string>,
|
|
1197
|
+
context: Context,
|
|
1198
|
+
options: SimpleStreamOptions,
|
|
1199
|
+
headers: Record<string, string>,
|
|
1200
|
+
includeReasoning: boolean,
|
|
1201
|
+
): Promise<ClineXmlResponseData> {
|
|
1202
|
+
const response = await fetch(`${BASE_URL_CLINE}/chat/completions`, {
|
|
1203
|
+
method: "POST",
|
|
1204
|
+
headers: {
|
|
1205
|
+
...headers,
|
|
1206
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
1207
|
+
"Content-Type": "application/json",
|
|
1208
|
+
},
|
|
1209
|
+
body: JSON.stringify({
|
|
1210
|
+
model: normalizeApiModelId(model.id),
|
|
1211
|
+
temperature: 0,
|
|
1212
|
+
messages: buildClineXmlMessages(context),
|
|
1213
|
+
stream: true,
|
|
1214
|
+
stream_options: { include_usage: true },
|
|
1215
|
+
...(includeReasoning ? { include_reasoning: true } : {}),
|
|
1216
|
+
}),
|
|
1217
|
+
signal: options.signal,
|
|
1218
|
+
});
|
|
1219
|
+
await options.onResponse?.(
|
|
1220
|
+
{
|
|
1221
|
+
status: response.status,
|
|
1222
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
1223
|
+
},
|
|
1224
|
+
model,
|
|
1225
|
+
);
|
|
1226
|
+
|
|
1227
|
+
if (!response.ok) {
|
|
1228
|
+
throw new Error(
|
|
1229
|
+
`Cline API error ${response.status}: ${await response.text()}`,
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return readClineXmlResponse(response);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
async function fetchClineXmlResponseWithReasoningFallback(
|
|
1237
|
+
model: Model<string>,
|
|
1238
|
+
context: Context,
|
|
1239
|
+
options: SimpleStreamOptions,
|
|
1240
|
+
headers: Record<string, string>,
|
|
1241
|
+
): Promise<ClineXmlResponseData> {
|
|
1242
|
+
try {
|
|
1243
|
+
return await fetchClineXmlResponse(model, context, options, headers, true);
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
if (
|
|
1246
|
+
options.signal?.aborted ||
|
|
1247
|
+
!isRetryableClineReasoningStreamError(error)
|
|
1248
|
+
) {
|
|
1249
|
+
throw error;
|
|
1250
|
+
}
|
|
1251
|
+
return fetchClineXmlResponse(model, context, options, headers, false);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
757
1255
|
function createAssistant(model: Model<string>): AssistantMessage {
|
|
758
1256
|
return {
|
|
759
1257
|
role: "assistant",
|
|
@@ -871,78 +1369,71 @@ export function streamClineXml(
|
|
|
871
1369
|
throw new Error("No Cline access token found. Run /login cline first.");
|
|
872
1370
|
}
|
|
873
1371
|
|
|
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 = "";
|
|
1372
|
+
let output: ReturnType<typeof prepareClineXmlOutput>;
|
|
1373
|
+
let rawText: string;
|
|
1374
|
+
let thinking: string;
|
|
907
1375
|
let finishReason: string | null | undefined;
|
|
908
1376
|
let usage: ClineXmlChunk["usage"] | undefined;
|
|
1377
|
+
let currentContext = context;
|
|
1378
|
+
|
|
1379
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1380
|
+
const data = await fetchClineXmlResponseWithReasoningFallback(
|
|
1381
|
+
model,
|
|
1382
|
+
currentContext,
|
|
1383
|
+
options,
|
|
1384
|
+
headers,
|
|
1385
|
+
);
|
|
1386
|
+
rawText = data.rawText;
|
|
1387
|
+
thinking = data.thinking;
|
|
1388
|
+
finishReason = data.finishReason;
|
|
1389
|
+
usage = data.usage;
|
|
1390
|
+
|
|
1391
|
+
const extractedThinking = extractThinkingXml(rawText);
|
|
1392
|
+
const parsedReasoning = parseReasoningToolCalls(
|
|
1393
|
+
thinking,
|
|
1394
|
+
currentContext.tools,
|
|
1395
|
+
);
|
|
1396
|
+
const parsed = parseXmlToolCalls(extractedThinking.text, currentContext.tools);
|
|
1397
|
+
output = prepareClineXmlOutput(
|
|
1398
|
+
parsed.text,
|
|
1399
|
+
extractedThinking.thinking,
|
|
1400
|
+
parsedReasoning.thinking,
|
|
1401
|
+
[...parsed.toolCalls, ...parsedReasoning.toolCalls],
|
|
1402
|
+
);
|
|
909
1403
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1404
|
+
// Reasoning-only response: MiMo stopped without producing visible
|
|
1405
|
+
// text or tool calls. Auto-retry once with a "continue" nudge
|
|
1406
|
+
// instead of showing a dead-end error to the user.
|
|
1407
|
+
if (
|
|
1408
|
+
output.visibleText === INTERNAL_ONLY_RESPONSE &&
|
|
1409
|
+
attempt === 0
|
|
1410
|
+
) {
|
|
1411
|
+
currentContext = {
|
|
1412
|
+
...context,
|
|
1413
|
+
messages: [
|
|
1414
|
+
...context.messages,
|
|
1415
|
+
{
|
|
1416
|
+
role: "user" as const,
|
|
1417
|
+
content: [{ type: "text" as const, text: "Please continue." }],
|
|
1418
|
+
timestamp: Date.now(),
|
|
1419
|
+
},
|
|
1420
|
+
],
|
|
1421
|
+
};
|
|
1422
|
+
continue;
|
|
915
1423
|
}
|
|
916
|
-
|
|
917
|
-
const choice = chunk.choices?.[0];
|
|
918
|
-
if (!choice) continue;
|
|
919
|
-
if (choice.error) {
|
|
920
|
-
throw new Error(
|
|
921
|
-
`${choice.error.code ?? "cline_error"}: ${choice.error.message ?? "Unknown Cline error"}`,
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
|
-
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
925
|
-
rawText += choice.delta?.content ?? "";
|
|
926
|
-
thinking += choice.delta?.reasoning ?? "";
|
|
1424
|
+
break;
|
|
927
1425
|
}
|
|
928
1426
|
|
|
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) {
|
|
1427
|
+
assistant.usage = usageFromChunkUsage(usage!);
|
|
1428
|
+
pushThinking(assistant, output!.thinkingText, stream);
|
|
1429
|
+
pushText(assistant, output!.visibleText, stream);
|
|
1430
|
+
const toolCalls = output!.toolCalls;
|
|
1431
|
+
for (const toolCall of toolCalls) {
|
|
941
1432
|
pushToolCall(assistant, toolCall, stream);
|
|
942
1433
|
}
|
|
943
1434
|
|
|
944
1435
|
assistant.stopReason =
|
|
945
|
-
|
|
1436
|
+
toolCalls.length > 0
|
|
946
1437
|
? "toolUse"
|
|
947
1438
|
: finishReason === "length"
|
|
948
1439
|
? "length"
|
|
@@ -969,6 +1460,12 @@ export function streamClineXml(
|
|
|
969
1460
|
|
|
970
1461
|
export const __test__ = {
|
|
971
1462
|
buildClineXmlMessages,
|
|
1463
|
+
extractFunctionTagToolCalls,
|
|
1464
|
+
isRetryableClineReasoningStreamError,
|
|
1465
|
+
normalizeDecoratedXmlTags,
|
|
1466
|
+
parseReasoningHiddenToolCalls,
|
|
1467
|
+
parseReasoningToolCalls,
|
|
972
1468
|
parseXmlToolCalls,
|
|
1469
|
+
prepareClineXmlOutput,
|
|
973
1470
|
serializeXmlToolCall,
|
|
974
1471
|
};
|