@mcp-use/inspector 7.0.0-canary.9 → 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.
Files changed (75) hide show
  1. package/README.md +3 -1
  2. package/dist/cdn/inspector.css +1 -1
  3. package/dist/cdn/inspector.js +15042 -14411
  4. package/dist/cli.js +477 -48
  5. package/dist/client/components/MCPAppsRenderer.d.ts +3 -1
  6. package/dist/client/components/MCPAppsRenderer.d.ts.map +1 -1
  7. package/dist/client/components/OpenAIComponentRenderer.d.ts.map +1 -1
  8. package/dist/client/components/chat/AssistantMessage.d.ts.map +1 -1
  9. package/dist/client/components/chat/ChatHeader.d.ts +5 -4
  10. package/dist/client/components/chat/ChatHeader.d.ts.map +1 -1
  11. package/dist/client/components/chat/ChatLandingForm.d.ts.map +1 -1
  12. package/dist/client/components/chat/ConfigurationDialog.d.ts +5 -5
  13. package/dist/client/components/chat/ConfigurationDialog.d.ts.map +1 -1
  14. package/dist/client/components/chat/CopyButton.d.ts +10 -0
  15. package/dist/client/components/chat/CopyButton.d.ts.map +1 -0
  16. package/dist/client/components/chat/UserMessage.d.ts.map +1 -1
  17. package/dist/client/components/chat/providerMeta.d.ts +7 -0
  18. package/dist/client/components/chat/providerMeta.d.ts.map +1 -0
  19. package/dist/client/components/chat/types.d.ts +6 -8
  20. package/dist/client/components/chat/types.d.ts.map +1 -1
  21. package/dist/client/components/chat/useConfig.d.ts +3 -2
  22. package/dist/client/components/chat/useConfig.d.ts.map +1 -1
  23. package/dist/client/components/tools/ToolInputForm.d.ts.map +1 -1
  24. package/dist/client/components/ui/SandboxedIframe.d.ts +2 -0
  25. package/dist/client/components/ui/SandboxedIframe.d.ts.map +1 -1
  26. package/dist/client/index.js +1769 -1107
  27. package/dist/client/lib/use-sandbox-remount-generation.d.ts +9 -0
  28. package/dist/client/lib/use-sandbox-remount-generation.d.ts.map +1 -0
  29. package/dist/client/lib/widget-fullscreen.d.ts +17 -0
  30. package/dist/client/lib/widget-fullscreen.d.ts.map +1 -0
  31. package/dist/client/telemetry/events.d.ts +2 -1
  32. package/dist/client/telemetry/events.d.ts.map +1 -1
  33. package/dist/llm/__tests__/messageFormat.test.d.ts +2 -0
  34. package/dist/llm/__tests__/messageFormat.test.d.ts.map +1 -0
  35. package/dist/llm/__tests__/providerToolResults.test.d.ts +2 -0
  36. package/dist/llm/__tests__/providerToolResults.test.d.ts.map +1 -0
  37. package/dist/llm/__tests__/toolResultParts.test.d.ts +2 -0
  38. package/dist/llm/__tests__/toolResultParts.test.d.ts.map +1 -0
  39. package/dist/llm/messageFormat.d.ts.map +1 -1
  40. package/dist/llm/ndjson.d.ts +5 -0
  41. package/dist/llm/ndjson.d.ts.map +1 -0
  42. package/dist/llm/providers/anthropic.d.ts +1 -0
  43. package/dist/llm/providers/anthropic.d.ts.map +1 -1
  44. package/dist/llm/providers/google.d.ts +1 -0
  45. package/dist/llm/providers/google.d.ts.map +1 -1
  46. package/dist/llm/providers/index.d.ts.map +1 -1
  47. package/dist/llm/providers/ollama/index.d.ts +18 -0
  48. package/dist/llm/providers/ollama/index.d.ts.map +1 -0
  49. package/dist/llm/providers/ollama/utils.d.ts +7 -0
  50. package/dist/llm/providers/ollama/utils.d.ts.map +1 -0
  51. package/dist/llm/providers/openai.d.ts +1 -0
  52. package/dist/llm/providers/openai.d.ts.map +1 -1
  53. package/dist/llm/toolLoop.d.ts.map +1 -1
  54. package/dist/llm/toolResultParts.d.ts +28 -0
  55. package/dist/llm/toolResultParts.d.ts.map +1 -0
  56. package/dist/llm/types.d.ts +1 -1
  57. package/dist/llm/types.d.ts.map +1 -1
  58. package/dist/server/{chunk-FWTBCKWN.js → chunk-A4VH7AIP.js} +1 -1
  59. package/dist/server/{chunk-POSNI7O3.js → chunk-PJNKFX2R.js} +477 -48
  60. package/dist/server/{chunk-2PI2LIIX.js → chunk-QNKR35CW.js} +1 -1
  61. package/dist/server/cli.js +2 -2
  62. package/dist/server/index.js +3 -3
  63. package/dist/server/middleware.js +3 -3
  64. package/dist/server/server.js +2 -2
  65. package/dist/server/shared-routes.js +2 -2
  66. package/dist/server/shared-utils.d.ts +2 -1
  67. package/dist/server/shared-utils.d.ts.map +1 -1
  68. package/dist/server/shared-utils.js +1 -1
  69. package/dist/web/assets/index-DB2aVJQh.css +2 -0
  70. package/dist/web/assets/{index-BGeNa-KU.js → index-MGfHvrb0.js} +118 -115
  71. package/dist/web/assets/module-BXKdgN2b.js +4 -0
  72. package/dist/web/index.html +3 -3
  73. package/package.json +5 -2
  74. package/dist/web/assets/index-BL3J9vaD.css +0 -2
  75. package/dist/web/assets/module-DjisYqbk.js +0 -4
package/dist/cli.js CHANGED
@@ -14,6 +14,142 @@ import open from "open";
14
14
  import { telFetch } from "mcp-use/telemetry/tel-fetch";
15
15
  import { mountMcpProxy, mountOAuthProxy } from "mcp-use/server";
16
16
 
17
+ // src/llm/toolResultParts.ts
18
+ function makeText(text) {
19
+ return { type: "text", text };
20
+ }
21
+ function makeImage(data, mimeType) {
22
+ return {
23
+ type: "image",
24
+ url: `data:${mimeType};base64,${data}`,
25
+ mimeType,
26
+ data
27
+ };
28
+ }
29
+ function isMcpResultShape(value) {
30
+ if (!value || typeof value !== "object") return false;
31
+ const v = value;
32
+ if (Array.isArray(v.content)) return true;
33
+ return "structuredContent" in v;
34
+ }
35
+ function isToolResultError(result) {
36
+ return typeof result === "object" && result !== null && result.isError === true;
37
+ }
38
+ function blockToPart(block) {
39
+ if (!block || typeof block !== "object") return null;
40
+ switch (block.type) {
41
+ case "text": {
42
+ const text = block.text;
43
+ return typeof text === "string" && text.length > 0 ? makeText(text) : null;
44
+ }
45
+ case "image": {
46
+ const b = block;
47
+ if (typeof b.data !== "string" || !b.data) return null;
48
+ const mime = typeof b.mimeType === "string" && b.mimeType ? b.mimeType : "image/png";
49
+ return makeImage(b.data, mime);
50
+ }
51
+ case "audio": {
52
+ const b = block;
53
+ const mime = typeof b.mimeType === "string" && b.mimeType ? b.mimeType : "audio/*";
54
+ const bytes = typeof b.data === "string" ? b.data.length : 0;
55
+ return makeText(`[audio: ${mime}, base64 omitted (${bytes} chars)]`);
56
+ }
57
+ case "resource": {
58
+ const r = block.resource ?? {};
59
+ if (typeof r.text === "string" && r.text.length > 0) {
60
+ return makeText(r.text);
61
+ }
62
+ if (typeof r.blob === "string" && typeof r.mimeType === "string" && r.mimeType.startsWith("image/")) {
63
+ return makeImage(r.blob, r.mimeType);
64
+ }
65
+ const uri = typeof r.uri === "string" ? r.uri : "<unknown>";
66
+ const mime = typeof r.mimeType === "string" ? r.mimeType : "unknown";
67
+ return makeText(`[resource: ${uri} (${mime})]`);
68
+ }
69
+ case "resource_link": {
70
+ const b = block;
71
+ const uri = typeof b.uri === "string" ? b.uri : "<unknown>";
72
+ const name = typeof b.name === "string" ? ` "${b.name}"` : "";
73
+ return makeText(`[resource_link${name}: ${uri}]`);
74
+ }
75
+ default: {
76
+ return makeText(
77
+ `[unsupported tool content block: ${String(block.type)}]`
78
+ );
79
+ }
80
+ }
81
+ }
82
+ function stripMeta(value) {
83
+ if (value && typeof value === "object" && !Array.isArray(value)) {
84
+ const { _meta: _ignored, ...rest } = value;
85
+ return rest;
86
+ }
87
+ return value;
88
+ }
89
+ function extractToolResultParts(result) {
90
+ const stripped = stripMeta(result);
91
+ if (typeof stripped === "string") {
92
+ return stripped.length > 0 ? [makeText(stripped)] : [];
93
+ }
94
+ if (isMcpResultShape(stripped)) {
95
+ const r = stripped;
96
+ const parts = [];
97
+ if (Array.isArray(r.content)) {
98
+ for (const block of r.content) {
99
+ const p = blockToPart(block);
100
+ if (p) parts.push(p);
101
+ }
102
+ }
103
+ if (r.structuredContent !== void 0) {
104
+ try {
105
+ parts.push(
106
+ makeText(`structuredContent: ${JSON.stringify(r.structuredContent)}`)
107
+ );
108
+ } catch {
109
+ }
110
+ }
111
+ if (r.isError) {
112
+ parts.unshift(makeText("[tool reported isError=true]"));
113
+ }
114
+ return parts;
115
+ }
116
+ try {
117
+ return [makeText(JSON.stringify(stripped))];
118
+ } catch {
119
+ return [makeText(String(stripped))];
120
+ }
121
+ }
122
+ function collapseToString(parts) {
123
+ if (parts.length === 0) return "";
124
+ if (parts.every((p) => p.type === "text")) {
125
+ return parts.map((p) => p.text).join("\n");
126
+ }
127
+ return null;
128
+ }
129
+ function toolResultToContent(result) {
130
+ const parts = extractToolResultParts(result);
131
+ const collapsed = collapseToString(parts);
132
+ return collapsed !== null ? collapsed : parts;
133
+ }
134
+ function partitionToolContent(content) {
135
+ if (typeof content === "string") {
136
+ return { text: content, imageParts: [], isString: true };
137
+ }
138
+ const texts = [];
139
+ const imageParts = [];
140
+ for (const p of content) {
141
+ if (p.type === "text") {
142
+ if (p.text) texts.push(p.text);
143
+ } else if (p.type === "image") {
144
+ imageParts.push(p);
145
+ }
146
+ }
147
+ return { text: texts.join("\n"), imageParts, isString: false };
148
+ }
149
+ function toolImageFollowupHeader(toolName, count) {
150
+ return `(Tool "${toolName ?? "<tool>"}" returned the following image${count === 1 ? "" : "s"}:)`;
151
+ }
152
+
17
153
  // src/llm/messageFormat.ts
18
154
  function extractText(m) {
19
155
  const raw = typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((x) => x?.text ?? "").join("\n") : JSON.stringify(m.content ?? "");
@@ -23,13 +159,6 @@ function extractText(m) {
23
159
  }
24
160
  return "";
25
161
  }
26
- function serializeToolResult(result) {
27
- if (result && typeof result === "object") {
28
- const { _meta: _ignored, ...rest } = result;
29
- return JSON.stringify(rest);
30
- }
31
- return typeof result === "string" ? result : JSON.stringify(result);
32
- }
33
162
  function convertMessagesToProvider(messages) {
34
163
  const out = [];
35
164
  messages.forEach((m, mi) => {
@@ -73,12 +202,14 @@ function convertMessagesToProvider(messages) {
73
202
  toolCalls
74
203
  });
75
204
  toolParts.forEach((p, i) => {
205
+ const result = p.toolInvocation.result;
76
206
  out.push({
77
207
  role: "tool",
78
- content: serializeToolResult(p.toolInvocation.result),
208
+ content: toolResultToContent(result),
79
209
  toolCallId: `call_${mi}_${i}_${p.toolInvocation.toolName}`,
80
210
  toolName: p.toolInvocation.toolName,
81
- toolResult: p.toolInvocation.result
211
+ toolResult: result,
212
+ toolIsError: isToolResultError(result)
82
213
  });
83
214
  });
84
215
  });
@@ -202,6 +333,24 @@ function toAnthropicContent(content) {
202
333
  if (blocks.length === 0) blocks.push({ type: "text", text: "[no content]" });
203
334
  return blocks;
204
335
  }
336
+ function toAnthropicToolResultContent(content) {
337
+ if (typeof content === "string") {
338
+ return content;
339
+ }
340
+ const blocks = [];
341
+ for (const p of content) {
342
+ if (p.type === "text") {
343
+ if (p.text) blocks.push({ type: "text", text: p.text });
344
+ } else if (p.type === "image") {
345
+ const img = buildImageBlock(p);
346
+ if (img) blocks.push(img);
347
+ }
348
+ }
349
+ if (blocks.length === 0) {
350
+ return "[no content]";
351
+ }
352
+ return blocks;
353
+ }
205
354
  function toAnthropicMessages(messages) {
206
355
  const out = [];
207
356
  for (const m of messages) {
@@ -210,7 +359,7 @@ function toAnthropicMessages(messages) {
210
359
  const block = {
211
360
  type: "tool_result",
212
361
  tool_use_id: m.toolCallId,
213
- content: typeof m.content === "string" ? m.content : JSON.stringify(m.toolResult ?? m.content),
362
+ content: toAnthropicToolResultContent(m.content),
214
363
  ...m.toolIsError ? { is_error: true } : {}
215
364
  };
216
365
  if (last && last.role === "user" && Array.isArray(last.content)) {
@@ -499,21 +648,39 @@ function toGeminiParts(content) {
499
648
  if (parts.length === 0) parts.push({ text: "[no content]" });
500
649
  return parts;
501
650
  }
651
+ function buildGeminiToolResponse(content) {
652
+ if (typeof content === "string") {
653
+ let parsed;
654
+ try {
655
+ parsed = JSON.parse(content);
656
+ } catch {
657
+ parsed = { result: content };
658
+ }
659
+ if (!parsed || typeof parsed !== "object") {
660
+ parsed = { result: parsed };
661
+ }
662
+ return { response: parsed, imageParts: [] };
663
+ }
664
+ const { text, imageParts } = partitionToolContent(content);
665
+ const response = {};
666
+ if (text) response.text = text;
667
+ if (imageParts.length > 0) {
668
+ response.images = imageParts.map((p) => ({
669
+ mimeType: p.mimeType ?? "image/*",
670
+ omitted: true
671
+ }));
672
+ response.note = "Image bytes were sent in the next user turn as inlineData.";
673
+ }
674
+ if (Object.keys(response).length === 0) {
675
+ response.result = null;
676
+ }
677
+ return { response, imageParts };
678
+ }
502
679
  function toGeminiContents(messages) {
503
680
  const out = [];
504
681
  for (const m of messages) {
505
682
  if (m.role === "tool") {
506
- let response;
507
- try {
508
- response = typeof m.content === "string" ? JSON.parse(m.content) : m.toolResult;
509
- } catch {
510
- response = {
511
- result: typeof m.content === "string" ? m.content : m.toolResult
512
- };
513
- }
514
- if (!response || typeof response !== "object") {
515
- response = { result: response };
516
- }
683
+ const { response, imageParts } = buildGeminiToolResponse(m.content);
517
684
  const last = out[out.length - 1];
518
685
  const part = {
519
686
  functionResponse: {
@@ -526,6 +693,16 @@ function toGeminiContents(messages) {
526
693
  } else {
527
694
  out.push({ role: "function", parts: [part] });
528
695
  }
696
+ if (imageParts.length > 0) {
697
+ const userParts = [
698
+ { text: toolImageFollowupHeader(m.toolName, imageParts.length) }
699
+ ];
700
+ for (const p of imageParts) {
701
+ const block = buildInlineData(p);
702
+ if (block) userParts.push(block);
703
+ }
704
+ out.push({ role: "user", parts: userParts });
705
+ }
529
706
  continue;
530
707
  }
531
708
  if (m.role === "assistant") {
@@ -671,12 +848,252 @@ async function chat2(params) {
671
848
  return { text, toolCalls };
672
849
  }
673
850
 
851
+ // src/llm/ndjson.ts
852
+ async function* parseNDJSON(body, signal) {
853
+ const reader = body.getReader();
854
+ const decoder = new TextDecoder();
855
+ let buffer = "";
856
+ try {
857
+ while (true) {
858
+ if (signal?.aborted) return;
859
+ const { done, value } = await reader.read();
860
+ if (done) break;
861
+ buffer += decoder.decode(value, { stream: true });
862
+ const lines = buffer.split("\n");
863
+ buffer = lines.pop() ?? "";
864
+ for (const line of lines) {
865
+ const trimmed = line.trim();
866
+ if (!trimmed) continue;
867
+ try {
868
+ yield JSON.parse(trimmed);
869
+ } catch {
870
+ }
871
+ }
872
+ }
873
+ const trailing = buffer.trim();
874
+ if (trailing) {
875
+ try {
876
+ yield JSON.parse(trailing);
877
+ } catch {
878
+ }
879
+ }
880
+ } finally {
881
+ reader.releaseLock();
882
+ }
883
+ }
884
+
885
+ // src/llm/providers/ollama/utils.ts
886
+ var DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434";
887
+ function normalizeOllamaBaseUrl(baseUrl) {
888
+ const raw = (baseUrl || DEFAULT_OLLAMA_BASE_URL).trim();
889
+ let end = raw.length;
890
+ while (end > 0 && raw.charCodeAt(end - 1) === 47) {
891
+ end--;
892
+ }
893
+ const trimmed = raw.slice(0, end);
894
+ return trimmed.endsWith("/api") ? trimmed.slice(0, -4) : trimmed;
895
+ }
896
+ function buildOllamaApiUrl(baseUrl, path) {
897
+ return `${normalizeOllamaBaseUrl(baseUrl)}${path}`;
898
+ }
899
+
900
+ // src/llm/providers/ollama/index.ts
901
+ function toOllamaImages(content) {
902
+ const images = [];
903
+ for (const part of content) {
904
+ if (part.type !== "image") continue;
905
+ if (part.data) {
906
+ images.push(part.data);
907
+ continue;
908
+ }
909
+ const parsed = parseDataUrl(part.url);
910
+ if (parsed?.data) {
911
+ images.push(parsed.data);
912
+ }
913
+ }
914
+ return images;
915
+ }
916
+ function toOllamaContent(content) {
917
+ if (typeof content === "string") {
918
+ return { content };
919
+ }
920
+ const text = content.filter(
921
+ (part) => part.type === "text"
922
+ ).map((part) => part.text).join("");
923
+ const images = toOllamaImages(content);
924
+ return {
925
+ content: text,
926
+ ...images.length > 0 ? { images } : {}
927
+ };
928
+ }
929
+ function toOllamaMessages(messages) {
930
+ return messages.map((message) => {
931
+ if (message.role === "tool") {
932
+ return {
933
+ role: "tool",
934
+ tool_name: message.toolName,
935
+ content: typeof message.content === "string" ? message.content : JSON.stringify(message.toolResult ?? message.content)
936
+ };
937
+ }
938
+ if (message.role === "assistant") {
939
+ const content = typeof message.content === "string" ? { content: message.content } : toOllamaContent(message.content);
940
+ return {
941
+ role: "assistant",
942
+ ...content,
943
+ ...message.toolCalls?.length ? {
944
+ tool_calls: message.toolCalls.map((toolCall, index) => ({
945
+ type: "function",
946
+ function: {
947
+ index,
948
+ name: toolCall.name,
949
+ arguments: toolCall.args
950
+ }
951
+ }))
952
+ } : {}
953
+ };
954
+ }
955
+ return {
956
+ role: message.role,
957
+ ...toOllamaContent(message.content)
958
+ };
959
+ });
960
+ }
961
+ function buildBody3(params, stream) {
962
+ const { config, messages, tools } = params;
963
+ const body = {
964
+ model: config.model,
965
+ messages: toOllamaMessages(messages),
966
+ stream
967
+ };
968
+ const options = {};
969
+ if (config.temperature !== void 0)
970
+ options.temperature = config.temperature;
971
+ if (config.maxTokens !== void 0) options.num_predict = config.maxTokens;
972
+ if (Object.keys(options).length > 0) body.options = options;
973
+ if (tools && tools.length > 0) {
974
+ body.tools = tools.map((tool) => ({
975
+ type: "function",
976
+ function: {
977
+ name: tool.name,
978
+ description: tool.description,
979
+ parameters: tool.inputSchema
980
+ }
981
+ }));
982
+ }
983
+ return body;
984
+ }
985
+ function buildHeaders(config) {
986
+ return {
987
+ "Content-Type": "application/json",
988
+ ...config.apiKey.trim() ? { Authorization: `Bearer ${config.apiKey.trim()}` } : {}
989
+ };
990
+ }
991
+ function normalizeToolCalls(toolCalls) {
992
+ if (!Array.isArray(toolCalls)) return [];
993
+ return toolCalls.map((toolCall) => {
994
+ const functionCall = toolCall && typeof toolCall === "object" ? toolCall.function : void 0;
995
+ const name = typeof functionCall?.name === "string" ? functionCall.name : "";
996
+ const rawArgs = functionCall?.arguments;
997
+ const args2 = rawArgs && typeof rawArgs === "object" ? rawArgs : {};
998
+ return {
999
+ name,
1000
+ args: args2
1001
+ };
1002
+ }).filter((toolCall) => toolCall.name);
1003
+ }
1004
+ async function* streamChat3(params) {
1005
+ const { config, signal } = params;
1006
+ const res = await fetch(buildOllamaApiUrl(config.baseUrl, "/api/chat"), {
1007
+ method: "POST",
1008
+ headers: buildHeaders(config),
1009
+ body: JSON.stringify(buildBody3(params, true)),
1010
+ signal
1011
+ });
1012
+ if (!res.ok || !res.body) {
1013
+ const text = await res.text().catch(() => "");
1014
+ throw new Error(
1015
+ `Ollama request failed (${res.status} ${res.statusText}): ${text}`
1016
+ );
1017
+ }
1018
+ let toolCallsEmitted = false;
1019
+ for await (const chunk of parseNDJSON(res.body, signal)) {
1020
+ const message = chunk && typeof chunk === "object" ? chunk.message : void 0;
1021
+ if (typeof message?.content === "string" && message.content.length > 0) {
1022
+ yield { type: "text-delta", delta: message.content };
1023
+ }
1024
+ if (!toolCallsEmitted) {
1025
+ const toolCalls = normalizeToolCalls(message?.tool_calls);
1026
+ if (toolCalls.length > 0) {
1027
+ for (const [index, toolCall] of toolCalls.entries()) {
1028
+ const toolCallId = `call_${index}_${toolCall.name || "tool"}`;
1029
+ yield {
1030
+ type: "tool-call-start",
1031
+ index,
1032
+ toolCallId,
1033
+ toolName: toolCall.name
1034
+ };
1035
+ const argsJson = JSON.stringify(toolCall.args);
1036
+ if (argsJson && argsJson !== "{}") {
1037
+ yield {
1038
+ type: "tool-call-args-delta",
1039
+ index,
1040
+ toolCallId,
1041
+ toolName: toolCall.name,
1042
+ argsDelta: argsJson
1043
+ };
1044
+ }
1045
+ yield {
1046
+ type: "tool-call-ready",
1047
+ index,
1048
+ toolCallId,
1049
+ toolName: toolCall.name,
1050
+ args: toolCall.args
1051
+ };
1052
+ }
1053
+ toolCallsEmitted = true;
1054
+ }
1055
+ }
1056
+ if (chunk && typeof chunk === "object" && chunk.error) {
1057
+ yield {
1058
+ type: "error",
1059
+ message: String(chunk.error)
1060
+ };
1061
+ }
1062
+ }
1063
+ yield { type: "done" };
1064
+ }
1065
+ async function chat3(params) {
1066
+ const { config, signal } = params;
1067
+ const res = await fetch(buildOllamaApiUrl(config.baseUrl, "/api/chat"), {
1068
+ method: "POST",
1069
+ headers: buildHeaders(config),
1070
+ body: JSON.stringify(buildBody3(params, false)),
1071
+ signal
1072
+ });
1073
+ if (!res.ok) {
1074
+ const text = await res.text().catch(() => "");
1075
+ throw new Error(
1076
+ `Ollama request failed (${res.status} ${res.statusText}): ${text}`
1077
+ );
1078
+ }
1079
+ const json = await res.json();
1080
+ const message = json?.message ?? {};
1081
+ return {
1082
+ text: typeof message.content === "string" ? message.content : "",
1083
+ toolCalls: normalizeToolCalls(message.tool_calls).map((tc, index) => ({
1084
+ id: `call_${index}_${tc.name || "tool"}`,
1085
+ name: tc.name,
1086
+ args: tc.args
1087
+ }))
1088
+ };
1089
+ }
1090
+
674
1091
  // src/llm/providers/openai.ts
675
1092
  var OPENAI_BASE_URL = "https://api.openai.com/v1";
676
1093
  function buildEndpoint(config, path) {
677
1094
  return `${config.baseUrl ?? OPENAI_BASE_URL}${path}`;
678
1095
  }
679
- function buildHeaders(config) {
1096
+ function buildHeaders2(config) {
680
1097
  const headers = {
681
1098
  "Content-Type": "application/json",
682
1099
  ...config.extraHeaders
@@ -697,11 +1114,26 @@ function toOpenAIMessages(messages) {
697
1114
  const out = [];
698
1115
  for (const m of messages) {
699
1116
  if (m.role === "tool") {
1117
+ const { text, imageParts } = partitionToolContent(m.content);
1118
+ const fallback = imageParts.length > 0 ? "[image content; see next message]" : "[no content]";
700
1119
  out.push({
701
1120
  role: "tool",
702
1121
  tool_call_id: m.toolCallId,
703
- content: typeof m.content === "string" ? m.content : JSON.stringify(m.toolResult ?? m.content)
1122
+ content: text || fallback
704
1123
  });
1124
+ if (imageParts.length > 0) {
1125
+ const userParts = [
1126
+ {
1127
+ type: "text",
1128
+ text: toolImageFollowupHeader(m.toolName, imageParts.length)
1129
+ },
1130
+ ...imageParts.map((p) => ({
1131
+ type: "image_url",
1132
+ image_url: { url: p.url }
1133
+ }))
1134
+ ];
1135
+ out.push({ role: "user", content: userParts });
1136
+ }
705
1137
  continue;
706
1138
  }
707
1139
  if (m.role === "assistant") {
@@ -721,10 +1153,9 @@ function toOpenAIMessages(messages) {
721
1153
  }
722
1154
  out.push({ role: m.role, content: toOpenAIContent(m.content) });
723
1155
  }
724
- void parseDataUrl;
725
1156
  return out;
726
1157
  }
727
- async function* streamChat3(params) {
1158
+ async function* streamChat4(params) {
728
1159
  const { config, messages, tools, signal } = params;
729
1160
  const body = {
730
1161
  model: config.model,
@@ -746,7 +1177,7 @@ async function* streamChat3(params) {
746
1177
  const endpoint = buildEndpoint(config, "/chat/completions");
747
1178
  const res = await fetch(endpoint, {
748
1179
  method: "POST",
749
- headers: buildHeaders(config),
1180
+ headers: buildHeaders2(config),
750
1181
  body: JSON.stringify(body),
751
1182
  signal
752
1183
  });
@@ -834,7 +1265,7 @@ async function* streamChat3(params) {
834
1265
  }
835
1266
  yield { type: "done" };
836
1267
  }
837
- async function chat3(params) {
1268
+ async function chat4(params) {
838
1269
  const { config, messages, tools, signal } = params;
839
1270
  const body = {
840
1271
  model: config.model,
@@ -855,7 +1286,7 @@ async function chat3(params) {
855
1286
  const endpoint = buildEndpoint(config, "/chat/completions");
856
1287
  const res = await fetch(endpoint, {
857
1288
  method: "POST",
858
- headers: buildHeaders(config),
1289
+ headers: buildHeaders2(config),
859
1290
  body: JSON.stringify(body),
860
1291
  signal
861
1292
  });
@@ -898,32 +1329,36 @@ function withOpenRouter(params) {
898
1329
  }
899
1330
  };
900
1331
  }
901
- function streamChat4(params) {
1332
+ function streamChat5(params) {
902
1333
  switch (params.config.provider) {
903
1334
  case "openai":
904
1335
  case "openai-compatible":
905
- return streamChat3(params);
1336
+ return streamChat4(params);
906
1337
  case "anthropic":
907
1338
  return streamChat(params);
908
1339
  case "google":
909
1340
  return streamChat2(params);
910
1341
  case "openrouter":
911
- return streamChat3(withOpenRouter(params));
1342
+ return streamChat4(withOpenRouter(params));
1343
+ case "ollama":
1344
+ return streamChat3(params);
912
1345
  default:
913
1346
  throw new Error(`Unsupported LLM provider: ${params.config.provider}`);
914
1347
  }
915
1348
  }
916
- function chat4(params) {
1349
+ function chat5(params) {
917
1350
  switch (params.config.provider) {
918
1351
  case "openai":
919
1352
  case "openai-compatible":
920
- return chat3(params);
1353
+ return chat4(params);
921
1354
  case "anthropic":
922
1355
  return chat(params);
923
1356
  case "google":
924
1357
  return chat2(params);
925
1358
  case "openrouter":
926
- return chat3(withOpenRouter(params));
1359
+ return chat4(withOpenRouter(params));
1360
+ case "ollama":
1361
+ return chat3(params);
927
1362
  default:
928
1363
  throw new Error(`Unsupported LLM provider: ${params.config.provider}`);
929
1364
  }
@@ -939,7 +1374,7 @@ async function* runToolLoop(params) {
939
1374
  const pendingToolCalls = [];
940
1375
  let assistantText = "";
941
1376
  try {
942
- for await (const ev of streamChat4({
1377
+ for await (const ev of streamChat5({
943
1378
  config,
944
1379
  messages,
945
1380
  tools,
@@ -977,17 +1412,17 @@ async function* runToolLoop(params) {
977
1412
  let isError = false;
978
1413
  try {
979
1414
  result = await callTool(tc.name, tc.args);
1415
+ isError = isToolResultError(result);
980
1416
  } catch (err) {
981
1417
  isError = true;
982
1418
  result = {
1419
+ isError: true,
983
1420
  error: err instanceof Error ? err.message : String(err)
984
1421
  };
985
1422
  }
986
- const normalized = stripMeta(result);
987
- const payloadText = typeof normalized === "string" ? normalized : JSON.stringify(normalized);
988
1423
  messages.push({
989
1424
  role: "tool",
990
- content: payloadText,
1425
+ content: toolResultToContent(result),
991
1426
  toolCallId: tc.id,
992
1427
  toolName: tc.name,
993
1428
  toolResult: result,
@@ -1003,13 +1438,6 @@ async function* runToolLoop(params) {
1003
1438
  }
1004
1439
  }
1005
1440
  }
1006
- function stripMeta(value) {
1007
- if (value && typeof value === "object" && !Array.isArray(value)) {
1008
- const { _meta: _ignored, ...rest } = value;
1009
- return rest;
1010
- }
1011
- return value;
1012
- }
1013
1441
  async function runToolLoopNonStreaming(params) {
1014
1442
  const { config, tools, callTool, signal } = params;
1015
1443
  const maxSteps = params.maxSteps ?? 10;
@@ -1018,7 +1446,7 @@ async function runToolLoopNonStreaming(params) {
1018
1446
  let finalText = "";
1019
1447
  for (let step = 0; step < maxSteps; step++) {
1020
1448
  if (signal?.aborted) break;
1021
- const { text, toolCalls } = await chat4({ config, messages, tools, signal });
1449
+ const { text, toolCalls } = await chat5({ config, messages, tools, signal });
1022
1450
  if (toolCalls.length === 0) {
1023
1451
  finalText = text;
1024
1452
  break;
@@ -1033,9 +1461,11 @@ async function runToolLoopNonStreaming(params) {
1033
1461
  let isError = false;
1034
1462
  try {
1035
1463
  result = await callTool(tc.name, tc.args);
1464
+ isError = isToolResultError(result);
1036
1465
  } catch (err) {
1037
1466
  isError = true;
1038
1467
  result = {
1468
+ isError: true,
1039
1469
  error: err instanceof Error ? err.message : String(err)
1040
1470
  };
1041
1471
  }
@@ -1044,10 +1474,9 @@ async function runToolLoopNonStreaming(params) {
1044
1474
  args: tc.args,
1045
1475
  result
1046
1476
  });
1047
- const payloadText = typeof result === "string" ? result : JSON.stringify(stripMeta(result));
1048
1477
  messages.push({
1049
1478
  role: "tool",
1050
- content: payloadText,
1479
+ content: toolResultToContent(result),
1051
1480
  toolCallId: tc.id,
1052
1481
  toolName: tc.name,
1053
1482
  toolResult: result,