@mcp-use/inspector 7.0.0-canary.8 → 7.0.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/README.md +3 -1
- package/dist/cdn/inspector.css +1 -1
- package/dist/cdn/inspector.js +15049 -14418
- package/dist/cli.js +477 -48
- package/dist/client/components/MCPAppsRenderer.d.ts +3 -1
- package/dist/client/components/MCPAppsRenderer.d.ts.map +1 -1
- package/dist/client/components/OpenAIComponentRenderer.d.ts.map +1 -1
- package/dist/client/components/chat/AssistantMessage.d.ts.map +1 -1
- package/dist/client/components/chat/ChatHeader.d.ts +5 -4
- package/dist/client/components/chat/ChatHeader.d.ts.map +1 -1
- package/dist/client/components/chat/ChatLandingForm.d.ts.map +1 -1
- package/dist/client/components/chat/ConfigurationDialog.d.ts +5 -5
- package/dist/client/components/chat/ConfigurationDialog.d.ts.map +1 -1
- package/dist/client/components/chat/CopyButton.d.ts +10 -0
- package/dist/client/components/chat/CopyButton.d.ts.map +1 -0
- package/dist/client/components/chat/UserMessage.d.ts.map +1 -1
- package/dist/client/components/chat/providerMeta.d.ts +7 -0
- package/dist/client/components/chat/providerMeta.d.ts.map +1 -0
- package/dist/client/components/chat/types.d.ts +6 -8
- package/dist/client/components/chat/types.d.ts.map +1 -1
- package/dist/client/components/chat/useConfig.d.ts +3 -2
- package/dist/client/components/chat/useConfig.d.ts.map +1 -1
- package/dist/client/components/shared/index.d.ts +0 -1
- package/dist/client/components/shared/index.d.ts.map +1 -1
- package/dist/client/components/tools/ToolInputForm.d.ts.map +1 -1
- package/dist/client/components/ui/SandboxedIframe.d.ts +2 -0
- package/dist/client/components/ui/SandboxedIframe.d.ts.map +1 -1
- package/dist/client/index.js +2059 -1400
- package/dist/client/lib/use-sandbox-remount-generation.d.ts +9 -0
- package/dist/client/lib/use-sandbox-remount-generation.d.ts.map +1 -0
- package/dist/client/lib/widget-fullscreen.d.ts +17 -0
- package/dist/client/lib/widget-fullscreen.d.ts.map +1 -0
- package/dist/client/telemetry/events.d.ts +2 -1
- package/dist/client/telemetry/events.d.ts.map +1 -1
- package/dist/llm/__tests__/messageFormat.test.d.ts +2 -0
- package/dist/llm/__tests__/messageFormat.test.d.ts.map +1 -0
- package/dist/llm/__tests__/providerToolResults.test.d.ts +2 -0
- package/dist/llm/__tests__/providerToolResults.test.d.ts.map +1 -0
- package/dist/llm/__tests__/toolResultParts.test.d.ts +2 -0
- package/dist/llm/__tests__/toolResultParts.test.d.ts.map +1 -0
- package/dist/llm/messageFormat.d.ts.map +1 -1
- package/dist/llm/ndjson.d.ts +5 -0
- package/dist/llm/ndjson.d.ts.map +1 -0
- package/dist/llm/providers/anthropic.d.ts +1 -0
- package/dist/llm/providers/anthropic.d.ts.map +1 -1
- package/dist/llm/providers/google.d.ts +1 -0
- package/dist/llm/providers/google.d.ts.map +1 -1
- package/dist/llm/providers/index.d.ts.map +1 -1
- package/dist/llm/providers/ollama/index.d.ts +18 -0
- package/dist/llm/providers/ollama/index.d.ts.map +1 -0
- package/dist/llm/providers/ollama/utils.d.ts +7 -0
- package/dist/llm/providers/ollama/utils.d.ts.map +1 -0
- package/dist/llm/providers/openai.d.ts +1 -0
- package/dist/llm/providers/openai.d.ts.map +1 -1
- package/dist/llm/toolLoop.d.ts.map +1 -1
- package/dist/llm/toolResultParts.d.ts +28 -0
- package/dist/llm/toolResultParts.d.ts.map +1 -0
- package/dist/llm/types.d.ts +1 -1
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/server/{chunk-FWTBCKWN.js → chunk-A4VH7AIP.js} +1 -1
- package/dist/server/{chunk-POSNI7O3.js → chunk-PJNKFX2R.js} +477 -48
- package/dist/server/{chunk-2PI2LIIX.js → chunk-QNKR35CW.js} +1 -1
- package/dist/server/cli.js +2 -2
- package/dist/server/index.js +3 -3
- package/dist/server/middleware.js +3 -3
- package/dist/server/server.js +2 -2
- package/dist/server/shared-routes.js +2 -2
- package/dist/server/shared-utils.d.ts +2 -1
- package/dist/server/shared-utils.d.ts.map +1 -1
- package/dist/server/shared-utils.js +1 -1
- package/dist/web/assets/index-DB2aVJQh.css +2 -0
- package/dist/web/assets/{index-B5Pi8EF8.js → index-MGfHvrb0.js} +118 -115
- package/dist/web/assets/module-BXKdgN2b.js +4 -0
- package/dist/web/index.html +3 -3
- package/package.json +5 -2
- package/dist/client/components/shared/TabCountBadge.d.ts +0 -9
- package/dist/client/components/shared/TabCountBadge.d.ts.map +0 -1
- package/dist/web/assets/index-BL3J9vaD.css +0 -2
- package/dist/web/assets/module-DjisYqbk.js +0 -4
|
@@ -1,3 +1,139 @@
|
|
|
1
|
+
// src/llm/toolResultParts.ts
|
|
2
|
+
function makeText(text) {
|
|
3
|
+
return { type: "text", text };
|
|
4
|
+
}
|
|
5
|
+
function makeImage(data, mimeType) {
|
|
6
|
+
return {
|
|
7
|
+
type: "image",
|
|
8
|
+
url: `data:${mimeType};base64,${data}`,
|
|
9
|
+
mimeType,
|
|
10
|
+
data
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function isMcpResultShape(value) {
|
|
14
|
+
if (!value || typeof value !== "object") return false;
|
|
15
|
+
const v = value;
|
|
16
|
+
if (Array.isArray(v.content)) return true;
|
|
17
|
+
return "structuredContent" in v;
|
|
18
|
+
}
|
|
19
|
+
function isToolResultError(result) {
|
|
20
|
+
return typeof result === "object" && result !== null && result.isError === true;
|
|
21
|
+
}
|
|
22
|
+
function blockToPart(block) {
|
|
23
|
+
if (!block || typeof block !== "object") return null;
|
|
24
|
+
switch (block.type) {
|
|
25
|
+
case "text": {
|
|
26
|
+
const text = block.text;
|
|
27
|
+
return typeof text === "string" && text.length > 0 ? makeText(text) : null;
|
|
28
|
+
}
|
|
29
|
+
case "image": {
|
|
30
|
+
const b = block;
|
|
31
|
+
if (typeof b.data !== "string" || !b.data) return null;
|
|
32
|
+
const mime = typeof b.mimeType === "string" && b.mimeType ? b.mimeType : "image/png";
|
|
33
|
+
return makeImage(b.data, mime);
|
|
34
|
+
}
|
|
35
|
+
case "audio": {
|
|
36
|
+
const b = block;
|
|
37
|
+
const mime = typeof b.mimeType === "string" && b.mimeType ? b.mimeType : "audio/*";
|
|
38
|
+
const bytes = typeof b.data === "string" ? b.data.length : 0;
|
|
39
|
+
return makeText(`[audio: ${mime}, base64 omitted (${bytes} chars)]`);
|
|
40
|
+
}
|
|
41
|
+
case "resource": {
|
|
42
|
+
const r = block.resource ?? {};
|
|
43
|
+
if (typeof r.text === "string" && r.text.length > 0) {
|
|
44
|
+
return makeText(r.text);
|
|
45
|
+
}
|
|
46
|
+
if (typeof r.blob === "string" && typeof r.mimeType === "string" && r.mimeType.startsWith("image/")) {
|
|
47
|
+
return makeImage(r.blob, r.mimeType);
|
|
48
|
+
}
|
|
49
|
+
const uri = typeof r.uri === "string" ? r.uri : "<unknown>";
|
|
50
|
+
const mime = typeof r.mimeType === "string" ? r.mimeType : "unknown";
|
|
51
|
+
return makeText(`[resource: ${uri} (${mime})]`);
|
|
52
|
+
}
|
|
53
|
+
case "resource_link": {
|
|
54
|
+
const b = block;
|
|
55
|
+
const uri = typeof b.uri === "string" ? b.uri : "<unknown>";
|
|
56
|
+
const name = typeof b.name === "string" ? ` "${b.name}"` : "";
|
|
57
|
+
return makeText(`[resource_link${name}: ${uri}]`);
|
|
58
|
+
}
|
|
59
|
+
default: {
|
|
60
|
+
return makeText(
|
|
61
|
+
`[unsupported tool content block: ${String(block.type)}]`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function stripMeta(value) {
|
|
67
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
68
|
+
const { _meta: _ignored, ...rest } = value;
|
|
69
|
+
return rest;
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
function extractToolResultParts(result) {
|
|
74
|
+
const stripped = stripMeta(result);
|
|
75
|
+
if (typeof stripped === "string") {
|
|
76
|
+
return stripped.length > 0 ? [makeText(stripped)] : [];
|
|
77
|
+
}
|
|
78
|
+
if (isMcpResultShape(stripped)) {
|
|
79
|
+
const r = stripped;
|
|
80
|
+
const parts = [];
|
|
81
|
+
if (Array.isArray(r.content)) {
|
|
82
|
+
for (const block of r.content) {
|
|
83
|
+
const p = blockToPart(block);
|
|
84
|
+
if (p) parts.push(p);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (r.structuredContent !== void 0) {
|
|
88
|
+
try {
|
|
89
|
+
parts.push(
|
|
90
|
+
makeText(`structuredContent: ${JSON.stringify(r.structuredContent)}`)
|
|
91
|
+
);
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (r.isError) {
|
|
96
|
+
parts.unshift(makeText("[tool reported isError=true]"));
|
|
97
|
+
}
|
|
98
|
+
return parts;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return [makeText(JSON.stringify(stripped))];
|
|
102
|
+
} catch {
|
|
103
|
+
return [makeText(String(stripped))];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function collapseToString(parts) {
|
|
107
|
+
if (parts.length === 0) return "";
|
|
108
|
+
if (parts.every((p) => p.type === "text")) {
|
|
109
|
+
return parts.map((p) => p.text).join("\n");
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function toolResultToContent(result) {
|
|
114
|
+
const parts = extractToolResultParts(result);
|
|
115
|
+
const collapsed = collapseToString(parts);
|
|
116
|
+
return collapsed !== null ? collapsed : parts;
|
|
117
|
+
}
|
|
118
|
+
function partitionToolContent(content) {
|
|
119
|
+
if (typeof content === "string") {
|
|
120
|
+
return { text: content, imageParts: [], isString: true };
|
|
121
|
+
}
|
|
122
|
+
const texts = [];
|
|
123
|
+
const imageParts = [];
|
|
124
|
+
for (const p of content) {
|
|
125
|
+
if (p.type === "text") {
|
|
126
|
+
if (p.text) texts.push(p.text);
|
|
127
|
+
} else if (p.type === "image") {
|
|
128
|
+
imageParts.push(p);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { text: texts.join("\n"), imageParts, isString: false };
|
|
132
|
+
}
|
|
133
|
+
function toolImageFollowupHeader(toolName, count) {
|
|
134
|
+
return `(Tool "${toolName ?? "<tool>"}" returned the following image${count === 1 ? "" : "s"}:)`;
|
|
135
|
+
}
|
|
136
|
+
|
|
1
137
|
// src/llm/messageFormat.ts
|
|
2
138
|
function extractText(m) {
|
|
3
139
|
const raw = typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((x) => x?.text ?? "").join("\n") : JSON.stringify(m.content ?? "");
|
|
@@ -7,13 +143,6 @@ function extractText(m) {
|
|
|
7
143
|
}
|
|
8
144
|
return "";
|
|
9
145
|
}
|
|
10
|
-
function serializeToolResult(result) {
|
|
11
|
-
if (result && typeof result === "object") {
|
|
12
|
-
const { _meta: _ignored, ...rest } = result;
|
|
13
|
-
return JSON.stringify(rest);
|
|
14
|
-
}
|
|
15
|
-
return typeof result === "string" ? result : JSON.stringify(result);
|
|
16
|
-
}
|
|
17
146
|
function convertMessagesToProvider(messages) {
|
|
18
147
|
const out = [];
|
|
19
148
|
messages.forEach((m, mi) => {
|
|
@@ -57,12 +186,14 @@ function convertMessagesToProvider(messages) {
|
|
|
57
186
|
toolCalls
|
|
58
187
|
});
|
|
59
188
|
toolParts.forEach((p, i) => {
|
|
189
|
+
const result = p.toolInvocation.result;
|
|
60
190
|
out.push({
|
|
61
191
|
role: "tool",
|
|
62
|
-
content:
|
|
192
|
+
content: toolResultToContent(result),
|
|
63
193
|
toolCallId: `call_${mi}_${i}_${p.toolInvocation.toolName}`,
|
|
64
194
|
toolName: p.toolInvocation.toolName,
|
|
65
|
-
toolResult:
|
|
195
|
+
toolResult: result,
|
|
196
|
+
toolIsError: isToolResultError(result)
|
|
66
197
|
});
|
|
67
198
|
});
|
|
68
199
|
});
|
|
@@ -186,6 +317,24 @@ function toAnthropicContent(content) {
|
|
|
186
317
|
if (blocks.length === 0) blocks.push({ type: "text", text: "[no content]" });
|
|
187
318
|
return blocks;
|
|
188
319
|
}
|
|
320
|
+
function toAnthropicToolResultContent(content) {
|
|
321
|
+
if (typeof content === "string") {
|
|
322
|
+
return content;
|
|
323
|
+
}
|
|
324
|
+
const blocks = [];
|
|
325
|
+
for (const p of content) {
|
|
326
|
+
if (p.type === "text") {
|
|
327
|
+
if (p.text) blocks.push({ type: "text", text: p.text });
|
|
328
|
+
} else if (p.type === "image") {
|
|
329
|
+
const img = buildImageBlock(p);
|
|
330
|
+
if (img) blocks.push(img);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (blocks.length === 0) {
|
|
334
|
+
return "[no content]";
|
|
335
|
+
}
|
|
336
|
+
return blocks;
|
|
337
|
+
}
|
|
189
338
|
function toAnthropicMessages(messages) {
|
|
190
339
|
const out = [];
|
|
191
340
|
for (const m of messages) {
|
|
@@ -194,7 +343,7 @@ function toAnthropicMessages(messages) {
|
|
|
194
343
|
const block = {
|
|
195
344
|
type: "tool_result",
|
|
196
345
|
tool_use_id: m.toolCallId,
|
|
197
|
-
content:
|
|
346
|
+
content: toAnthropicToolResultContent(m.content),
|
|
198
347
|
...m.toolIsError ? { is_error: true } : {}
|
|
199
348
|
};
|
|
200
349
|
if (last && last.role === "user" && Array.isArray(last.content)) {
|
|
@@ -483,21 +632,39 @@ function toGeminiParts(content) {
|
|
|
483
632
|
if (parts.length === 0) parts.push({ text: "[no content]" });
|
|
484
633
|
return parts;
|
|
485
634
|
}
|
|
635
|
+
function buildGeminiToolResponse(content) {
|
|
636
|
+
if (typeof content === "string") {
|
|
637
|
+
let parsed;
|
|
638
|
+
try {
|
|
639
|
+
parsed = JSON.parse(content);
|
|
640
|
+
} catch {
|
|
641
|
+
parsed = { result: content };
|
|
642
|
+
}
|
|
643
|
+
if (!parsed || typeof parsed !== "object") {
|
|
644
|
+
parsed = { result: parsed };
|
|
645
|
+
}
|
|
646
|
+
return { response: parsed, imageParts: [] };
|
|
647
|
+
}
|
|
648
|
+
const { text, imageParts } = partitionToolContent(content);
|
|
649
|
+
const response = {};
|
|
650
|
+
if (text) response.text = text;
|
|
651
|
+
if (imageParts.length > 0) {
|
|
652
|
+
response.images = imageParts.map((p) => ({
|
|
653
|
+
mimeType: p.mimeType ?? "image/*",
|
|
654
|
+
omitted: true
|
|
655
|
+
}));
|
|
656
|
+
response.note = "Image bytes were sent in the next user turn as inlineData.";
|
|
657
|
+
}
|
|
658
|
+
if (Object.keys(response).length === 0) {
|
|
659
|
+
response.result = null;
|
|
660
|
+
}
|
|
661
|
+
return { response, imageParts };
|
|
662
|
+
}
|
|
486
663
|
function toGeminiContents(messages) {
|
|
487
664
|
const out = [];
|
|
488
665
|
for (const m of messages) {
|
|
489
666
|
if (m.role === "tool") {
|
|
490
|
-
|
|
491
|
-
try {
|
|
492
|
-
response = typeof m.content === "string" ? JSON.parse(m.content) : m.toolResult;
|
|
493
|
-
} catch {
|
|
494
|
-
response = {
|
|
495
|
-
result: typeof m.content === "string" ? m.content : m.toolResult
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
if (!response || typeof response !== "object") {
|
|
499
|
-
response = { result: response };
|
|
500
|
-
}
|
|
667
|
+
const { response, imageParts } = buildGeminiToolResponse(m.content);
|
|
501
668
|
const last = out[out.length - 1];
|
|
502
669
|
const part = {
|
|
503
670
|
functionResponse: {
|
|
@@ -510,6 +677,16 @@ function toGeminiContents(messages) {
|
|
|
510
677
|
} else {
|
|
511
678
|
out.push({ role: "function", parts: [part] });
|
|
512
679
|
}
|
|
680
|
+
if (imageParts.length > 0) {
|
|
681
|
+
const userParts = [
|
|
682
|
+
{ text: toolImageFollowupHeader(m.toolName, imageParts.length) }
|
|
683
|
+
];
|
|
684
|
+
for (const p of imageParts) {
|
|
685
|
+
const block = buildInlineData(p);
|
|
686
|
+
if (block) userParts.push(block);
|
|
687
|
+
}
|
|
688
|
+
out.push({ role: "user", parts: userParts });
|
|
689
|
+
}
|
|
513
690
|
continue;
|
|
514
691
|
}
|
|
515
692
|
if (m.role === "assistant") {
|
|
@@ -655,12 +832,252 @@ async function chat2(params) {
|
|
|
655
832
|
return { text, toolCalls };
|
|
656
833
|
}
|
|
657
834
|
|
|
835
|
+
// src/llm/ndjson.ts
|
|
836
|
+
async function* parseNDJSON(body, signal) {
|
|
837
|
+
const reader = body.getReader();
|
|
838
|
+
const decoder = new TextDecoder();
|
|
839
|
+
let buffer = "";
|
|
840
|
+
try {
|
|
841
|
+
while (true) {
|
|
842
|
+
if (signal?.aborted) return;
|
|
843
|
+
const { done, value } = await reader.read();
|
|
844
|
+
if (done) break;
|
|
845
|
+
buffer += decoder.decode(value, { stream: true });
|
|
846
|
+
const lines = buffer.split("\n");
|
|
847
|
+
buffer = lines.pop() ?? "";
|
|
848
|
+
for (const line of lines) {
|
|
849
|
+
const trimmed = line.trim();
|
|
850
|
+
if (!trimmed) continue;
|
|
851
|
+
try {
|
|
852
|
+
yield JSON.parse(trimmed);
|
|
853
|
+
} catch {
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
const trailing = buffer.trim();
|
|
858
|
+
if (trailing) {
|
|
859
|
+
try {
|
|
860
|
+
yield JSON.parse(trailing);
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
} finally {
|
|
865
|
+
reader.releaseLock();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// src/llm/providers/ollama/utils.ts
|
|
870
|
+
var DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434";
|
|
871
|
+
function normalizeOllamaBaseUrl(baseUrl) {
|
|
872
|
+
const raw = (baseUrl || DEFAULT_OLLAMA_BASE_URL).trim();
|
|
873
|
+
let end = raw.length;
|
|
874
|
+
while (end > 0 && raw.charCodeAt(end - 1) === 47) {
|
|
875
|
+
end--;
|
|
876
|
+
}
|
|
877
|
+
const trimmed = raw.slice(0, end);
|
|
878
|
+
return trimmed.endsWith("/api") ? trimmed.slice(0, -4) : trimmed;
|
|
879
|
+
}
|
|
880
|
+
function buildOllamaApiUrl(baseUrl, path) {
|
|
881
|
+
return `${normalizeOllamaBaseUrl(baseUrl)}${path}`;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/llm/providers/ollama/index.ts
|
|
885
|
+
function toOllamaImages(content) {
|
|
886
|
+
const images = [];
|
|
887
|
+
for (const part of content) {
|
|
888
|
+
if (part.type !== "image") continue;
|
|
889
|
+
if (part.data) {
|
|
890
|
+
images.push(part.data);
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
const parsed = parseDataUrl(part.url);
|
|
894
|
+
if (parsed?.data) {
|
|
895
|
+
images.push(parsed.data);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return images;
|
|
899
|
+
}
|
|
900
|
+
function toOllamaContent(content) {
|
|
901
|
+
if (typeof content === "string") {
|
|
902
|
+
return { content };
|
|
903
|
+
}
|
|
904
|
+
const text = content.filter(
|
|
905
|
+
(part) => part.type === "text"
|
|
906
|
+
).map((part) => part.text).join("");
|
|
907
|
+
const images = toOllamaImages(content);
|
|
908
|
+
return {
|
|
909
|
+
content: text,
|
|
910
|
+
...images.length > 0 ? { images } : {}
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function toOllamaMessages(messages) {
|
|
914
|
+
return messages.map((message) => {
|
|
915
|
+
if (message.role === "tool") {
|
|
916
|
+
return {
|
|
917
|
+
role: "tool",
|
|
918
|
+
tool_name: message.toolName,
|
|
919
|
+
content: typeof message.content === "string" ? message.content : JSON.stringify(message.toolResult ?? message.content)
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
if (message.role === "assistant") {
|
|
923
|
+
const content = typeof message.content === "string" ? { content: message.content } : toOllamaContent(message.content);
|
|
924
|
+
return {
|
|
925
|
+
role: "assistant",
|
|
926
|
+
...content,
|
|
927
|
+
...message.toolCalls?.length ? {
|
|
928
|
+
tool_calls: message.toolCalls.map((toolCall, index) => ({
|
|
929
|
+
type: "function",
|
|
930
|
+
function: {
|
|
931
|
+
index,
|
|
932
|
+
name: toolCall.name,
|
|
933
|
+
arguments: toolCall.args
|
|
934
|
+
}
|
|
935
|
+
}))
|
|
936
|
+
} : {}
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
return {
|
|
940
|
+
role: message.role,
|
|
941
|
+
...toOllamaContent(message.content)
|
|
942
|
+
};
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
function buildBody3(params, stream) {
|
|
946
|
+
const { config, messages, tools } = params;
|
|
947
|
+
const body = {
|
|
948
|
+
model: config.model,
|
|
949
|
+
messages: toOllamaMessages(messages),
|
|
950
|
+
stream
|
|
951
|
+
};
|
|
952
|
+
const options = {};
|
|
953
|
+
if (config.temperature !== void 0)
|
|
954
|
+
options.temperature = config.temperature;
|
|
955
|
+
if (config.maxTokens !== void 0) options.num_predict = config.maxTokens;
|
|
956
|
+
if (Object.keys(options).length > 0) body.options = options;
|
|
957
|
+
if (tools && tools.length > 0) {
|
|
958
|
+
body.tools = tools.map((tool) => ({
|
|
959
|
+
type: "function",
|
|
960
|
+
function: {
|
|
961
|
+
name: tool.name,
|
|
962
|
+
description: tool.description,
|
|
963
|
+
parameters: tool.inputSchema
|
|
964
|
+
}
|
|
965
|
+
}));
|
|
966
|
+
}
|
|
967
|
+
return body;
|
|
968
|
+
}
|
|
969
|
+
function buildHeaders(config) {
|
|
970
|
+
return {
|
|
971
|
+
"Content-Type": "application/json",
|
|
972
|
+
...config.apiKey.trim() ? { Authorization: `Bearer ${config.apiKey.trim()}` } : {}
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
function normalizeToolCalls(toolCalls) {
|
|
976
|
+
if (!Array.isArray(toolCalls)) return [];
|
|
977
|
+
return toolCalls.map((toolCall) => {
|
|
978
|
+
const functionCall = toolCall && typeof toolCall === "object" ? toolCall.function : void 0;
|
|
979
|
+
const name = typeof functionCall?.name === "string" ? functionCall.name : "";
|
|
980
|
+
const rawArgs = functionCall?.arguments;
|
|
981
|
+
const args = rawArgs && typeof rawArgs === "object" ? rawArgs : {};
|
|
982
|
+
return {
|
|
983
|
+
name,
|
|
984
|
+
args
|
|
985
|
+
};
|
|
986
|
+
}).filter((toolCall) => toolCall.name);
|
|
987
|
+
}
|
|
988
|
+
async function* streamChat3(params) {
|
|
989
|
+
const { config, signal } = params;
|
|
990
|
+
const res = await fetch(buildOllamaApiUrl(config.baseUrl, "/api/chat"), {
|
|
991
|
+
method: "POST",
|
|
992
|
+
headers: buildHeaders(config),
|
|
993
|
+
body: JSON.stringify(buildBody3(params, true)),
|
|
994
|
+
signal
|
|
995
|
+
});
|
|
996
|
+
if (!res.ok || !res.body) {
|
|
997
|
+
const text = await res.text().catch(() => "");
|
|
998
|
+
throw new Error(
|
|
999
|
+
`Ollama request failed (${res.status} ${res.statusText}): ${text}`
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
let toolCallsEmitted = false;
|
|
1003
|
+
for await (const chunk of parseNDJSON(res.body, signal)) {
|
|
1004
|
+
const message = chunk && typeof chunk === "object" ? chunk.message : void 0;
|
|
1005
|
+
if (typeof message?.content === "string" && message.content.length > 0) {
|
|
1006
|
+
yield { type: "text-delta", delta: message.content };
|
|
1007
|
+
}
|
|
1008
|
+
if (!toolCallsEmitted) {
|
|
1009
|
+
const toolCalls = normalizeToolCalls(message?.tool_calls);
|
|
1010
|
+
if (toolCalls.length > 0) {
|
|
1011
|
+
for (const [index, toolCall] of toolCalls.entries()) {
|
|
1012
|
+
const toolCallId = `call_${index}_${toolCall.name || "tool"}`;
|
|
1013
|
+
yield {
|
|
1014
|
+
type: "tool-call-start",
|
|
1015
|
+
index,
|
|
1016
|
+
toolCallId,
|
|
1017
|
+
toolName: toolCall.name
|
|
1018
|
+
};
|
|
1019
|
+
const argsJson = JSON.stringify(toolCall.args);
|
|
1020
|
+
if (argsJson && argsJson !== "{}") {
|
|
1021
|
+
yield {
|
|
1022
|
+
type: "tool-call-args-delta",
|
|
1023
|
+
index,
|
|
1024
|
+
toolCallId,
|
|
1025
|
+
toolName: toolCall.name,
|
|
1026
|
+
argsDelta: argsJson
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
yield {
|
|
1030
|
+
type: "tool-call-ready",
|
|
1031
|
+
index,
|
|
1032
|
+
toolCallId,
|
|
1033
|
+
toolName: toolCall.name,
|
|
1034
|
+
args: toolCall.args
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
toolCallsEmitted = true;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (chunk && typeof chunk === "object" && chunk.error) {
|
|
1041
|
+
yield {
|
|
1042
|
+
type: "error",
|
|
1043
|
+
message: String(chunk.error)
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
yield { type: "done" };
|
|
1048
|
+
}
|
|
1049
|
+
async function chat3(params) {
|
|
1050
|
+
const { config, signal } = params;
|
|
1051
|
+
const res = await fetch(buildOllamaApiUrl(config.baseUrl, "/api/chat"), {
|
|
1052
|
+
method: "POST",
|
|
1053
|
+
headers: buildHeaders(config),
|
|
1054
|
+
body: JSON.stringify(buildBody3(params, false)),
|
|
1055
|
+
signal
|
|
1056
|
+
});
|
|
1057
|
+
if (!res.ok) {
|
|
1058
|
+
const text = await res.text().catch(() => "");
|
|
1059
|
+
throw new Error(
|
|
1060
|
+
`Ollama request failed (${res.status} ${res.statusText}): ${text}`
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
const json = await res.json();
|
|
1064
|
+
const message = json?.message ?? {};
|
|
1065
|
+
return {
|
|
1066
|
+
text: typeof message.content === "string" ? message.content : "",
|
|
1067
|
+
toolCalls: normalizeToolCalls(message.tool_calls).map((tc, index) => ({
|
|
1068
|
+
id: `call_${index}_${tc.name || "tool"}`,
|
|
1069
|
+
name: tc.name,
|
|
1070
|
+
args: tc.args
|
|
1071
|
+
}))
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
658
1075
|
// src/llm/providers/openai.ts
|
|
659
1076
|
var OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
660
1077
|
function buildEndpoint(config, path) {
|
|
661
1078
|
return `${config.baseUrl ?? OPENAI_BASE_URL}${path}`;
|
|
662
1079
|
}
|
|
663
|
-
function
|
|
1080
|
+
function buildHeaders2(config) {
|
|
664
1081
|
const headers = {
|
|
665
1082
|
"Content-Type": "application/json",
|
|
666
1083
|
...config.extraHeaders
|
|
@@ -681,11 +1098,26 @@ function toOpenAIMessages(messages) {
|
|
|
681
1098
|
const out = [];
|
|
682
1099
|
for (const m of messages) {
|
|
683
1100
|
if (m.role === "tool") {
|
|
1101
|
+
const { text, imageParts } = partitionToolContent(m.content);
|
|
1102
|
+
const fallback = imageParts.length > 0 ? "[image content; see next message]" : "[no content]";
|
|
684
1103
|
out.push({
|
|
685
1104
|
role: "tool",
|
|
686
1105
|
tool_call_id: m.toolCallId,
|
|
687
|
-
content:
|
|
1106
|
+
content: text || fallback
|
|
688
1107
|
});
|
|
1108
|
+
if (imageParts.length > 0) {
|
|
1109
|
+
const userParts = [
|
|
1110
|
+
{
|
|
1111
|
+
type: "text",
|
|
1112
|
+
text: toolImageFollowupHeader(m.toolName, imageParts.length)
|
|
1113
|
+
},
|
|
1114
|
+
...imageParts.map((p) => ({
|
|
1115
|
+
type: "image_url",
|
|
1116
|
+
image_url: { url: p.url }
|
|
1117
|
+
}))
|
|
1118
|
+
];
|
|
1119
|
+
out.push({ role: "user", content: userParts });
|
|
1120
|
+
}
|
|
689
1121
|
continue;
|
|
690
1122
|
}
|
|
691
1123
|
if (m.role === "assistant") {
|
|
@@ -705,10 +1137,9 @@ function toOpenAIMessages(messages) {
|
|
|
705
1137
|
}
|
|
706
1138
|
out.push({ role: m.role, content: toOpenAIContent(m.content) });
|
|
707
1139
|
}
|
|
708
|
-
void parseDataUrl;
|
|
709
1140
|
return out;
|
|
710
1141
|
}
|
|
711
|
-
async function*
|
|
1142
|
+
async function* streamChat4(params) {
|
|
712
1143
|
const { config, messages, tools, signal } = params;
|
|
713
1144
|
const body = {
|
|
714
1145
|
model: config.model,
|
|
@@ -730,7 +1161,7 @@ async function* streamChat3(params) {
|
|
|
730
1161
|
const endpoint = buildEndpoint(config, "/chat/completions");
|
|
731
1162
|
const res = await fetch(endpoint, {
|
|
732
1163
|
method: "POST",
|
|
733
|
-
headers:
|
|
1164
|
+
headers: buildHeaders2(config),
|
|
734
1165
|
body: JSON.stringify(body),
|
|
735
1166
|
signal
|
|
736
1167
|
});
|
|
@@ -818,7 +1249,7 @@ async function* streamChat3(params) {
|
|
|
818
1249
|
}
|
|
819
1250
|
yield { type: "done" };
|
|
820
1251
|
}
|
|
821
|
-
async function
|
|
1252
|
+
async function chat4(params) {
|
|
822
1253
|
const { config, messages, tools, signal } = params;
|
|
823
1254
|
const body = {
|
|
824
1255
|
model: config.model,
|
|
@@ -839,7 +1270,7 @@ async function chat3(params) {
|
|
|
839
1270
|
const endpoint = buildEndpoint(config, "/chat/completions");
|
|
840
1271
|
const res = await fetch(endpoint, {
|
|
841
1272
|
method: "POST",
|
|
842
|
-
headers:
|
|
1273
|
+
headers: buildHeaders2(config),
|
|
843
1274
|
body: JSON.stringify(body),
|
|
844
1275
|
signal
|
|
845
1276
|
});
|
|
@@ -882,32 +1313,36 @@ function withOpenRouter(params) {
|
|
|
882
1313
|
}
|
|
883
1314
|
};
|
|
884
1315
|
}
|
|
885
|
-
function
|
|
1316
|
+
function streamChat5(params) {
|
|
886
1317
|
switch (params.config.provider) {
|
|
887
1318
|
case "openai":
|
|
888
1319
|
case "openai-compatible":
|
|
889
|
-
return
|
|
1320
|
+
return streamChat4(params);
|
|
890
1321
|
case "anthropic":
|
|
891
1322
|
return streamChat(params);
|
|
892
1323
|
case "google":
|
|
893
1324
|
return streamChat2(params);
|
|
894
1325
|
case "openrouter":
|
|
895
|
-
return
|
|
1326
|
+
return streamChat4(withOpenRouter(params));
|
|
1327
|
+
case "ollama":
|
|
1328
|
+
return streamChat3(params);
|
|
896
1329
|
default:
|
|
897
1330
|
throw new Error(`Unsupported LLM provider: ${params.config.provider}`);
|
|
898
1331
|
}
|
|
899
1332
|
}
|
|
900
|
-
function
|
|
1333
|
+
function chat5(params) {
|
|
901
1334
|
switch (params.config.provider) {
|
|
902
1335
|
case "openai":
|
|
903
1336
|
case "openai-compatible":
|
|
904
|
-
return
|
|
1337
|
+
return chat4(params);
|
|
905
1338
|
case "anthropic":
|
|
906
1339
|
return chat(params);
|
|
907
1340
|
case "google":
|
|
908
1341
|
return chat2(params);
|
|
909
1342
|
case "openrouter":
|
|
910
|
-
return
|
|
1343
|
+
return chat4(withOpenRouter(params));
|
|
1344
|
+
case "ollama":
|
|
1345
|
+
return chat3(params);
|
|
911
1346
|
default:
|
|
912
1347
|
throw new Error(`Unsupported LLM provider: ${params.config.provider}`);
|
|
913
1348
|
}
|
|
@@ -923,7 +1358,7 @@ async function* runToolLoop(params) {
|
|
|
923
1358
|
const pendingToolCalls = [];
|
|
924
1359
|
let assistantText = "";
|
|
925
1360
|
try {
|
|
926
|
-
for await (const ev of
|
|
1361
|
+
for await (const ev of streamChat5({
|
|
927
1362
|
config,
|
|
928
1363
|
messages,
|
|
929
1364
|
tools,
|
|
@@ -961,17 +1396,17 @@ async function* runToolLoop(params) {
|
|
|
961
1396
|
let isError = false;
|
|
962
1397
|
try {
|
|
963
1398
|
result = await callTool(tc.name, tc.args);
|
|
1399
|
+
isError = isToolResultError(result);
|
|
964
1400
|
} catch (err) {
|
|
965
1401
|
isError = true;
|
|
966
1402
|
result = {
|
|
1403
|
+
isError: true,
|
|
967
1404
|
error: err instanceof Error ? err.message : String(err)
|
|
968
1405
|
};
|
|
969
1406
|
}
|
|
970
|
-
const normalized = stripMeta(result);
|
|
971
|
-
const payloadText = typeof normalized === "string" ? normalized : JSON.stringify(normalized);
|
|
972
1407
|
messages.push({
|
|
973
1408
|
role: "tool",
|
|
974
|
-
content:
|
|
1409
|
+
content: toolResultToContent(result),
|
|
975
1410
|
toolCallId: tc.id,
|
|
976
1411
|
toolName: tc.name,
|
|
977
1412
|
toolResult: result,
|
|
@@ -987,13 +1422,6 @@ async function* runToolLoop(params) {
|
|
|
987
1422
|
}
|
|
988
1423
|
}
|
|
989
1424
|
}
|
|
990
|
-
function stripMeta(value) {
|
|
991
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
992
|
-
const { _meta: _ignored, ...rest } = value;
|
|
993
|
-
return rest;
|
|
994
|
-
}
|
|
995
|
-
return value;
|
|
996
|
-
}
|
|
997
1425
|
async function runToolLoopNonStreaming(params) {
|
|
998
1426
|
const { config, tools, callTool, signal } = params;
|
|
999
1427
|
const maxSteps = params.maxSteps ?? 10;
|
|
@@ -1002,7 +1430,7 @@ async function runToolLoopNonStreaming(params) {
|
|
|
1002
1430
|
let finalText = "";
|
|
1003
1431
|
for (let step = 0; step < maxSteps; step++) {
|
|
1004
1432
|
if (signal?.aborted) break;
|
|
1005
|
-
const { text, toolCalls } = await
|
|
1433
|
+
const { text, toolCalls } = await chat5({ config, messages, tools, signal });
|
|
1006
1434
|
if (toolCalls.length === 0) {
|
|
1007
1435
|
finalText = text;
|
|
1008
1436
|
break;
|
|
@@ -1017,9 +1445,11 @@ async function runToolLoopNonStreaming(params) {
|
|
|
1017
1445
|
let isError = false;
|
|
1018
1446
|
try {
|
|
1019
1447
|
result = await callTool(tc.name, tc.args);
|
|
1448
|
+
isError = isToolResultError(result);
|
|
1020
1449
|
} catch (err) {
|
|
1021
1450
|
isError = true;
|
|
1022
1451
|
result = {
|
|
1452
|
+
isError: true,
|
|
1023
1453
|
error: err instanceof Error ? err.message : String(err)
|
|
1024
1454
|
};
|
|
1025
1455
|
}
|
|
@@ -1028,10 +1458,9 @@ async function runToolLoopNonStreaming(params) {
|
|
|
1028
1458
|
args: tc.args,
|
|
1029
1459
|
result
|
|
1030
1460
|
});
|
|
1031
|
-
const payloadText = typeof result === "string" ? result : JSON.stringify(stripMeta(result));
|
|
1032
1461
|
messages.push({
|
|
1033
1462
|
role: "tool",
|
|
1034
|
-
content:
|
|
1463
|
+
content: toolResultToContent(result),
|
|
1035
1464
|
toolCallId: tc.id,
|
|
1036
1465
|
toolName: tc.name,
|
|
1037
1466
|
toolResult: result,
|