@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.
Files changed (79) hide show
  1. package/README.md +3 -1
  2. package/dist/cdn/inspector.css +1 -1
  3. package/dist/cdn/inspector.js +15049 -14418
  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/shared/index.d.ts +0 -1
  24. package/dist/client/components/shared/index.d.ts.map +1 -1
  25. package/dist/client/components/tools/ToolInputForm.d.ts.map +1 -1
  26. package/dist/client/components/ui/SandboxedIframe.d.ts +2 -0
  27. package/dist/client/components/ui/SandboxedIframe.d.ts.map +1 -1
  28. package/dist/client/index.js +2059 -1400
  29. package/dist/client/lib/use-sandbox-remount-generation.d.ts +9 -0
  30. package/dist/client/lib/use-sandbox-remount-generation.d.ts.map +1 -0
  31. package/dist/client/lib/widget-fullscreen.d.ts +17 -0
  32. package/dist/client/lib/widget-fullscreen.d.ts.map +1 -0
  33. package/dist/client/telemetry/events.d.ts +2 -1
  34. package/dist/client/telemetry/events.d.ts.map +1 -1
  35. package/dist/llm/__tests__/messageFormat.test.d.ts +2 -0
  36. package/dist/llm/__tests__/messageFormat.test.d.ts.map +1 -0
  37. package/dist/llm/__tests__/providerToolResults.test.d.ts +2 -0
  38. package/dist/llm/__tests__/providerToolResults.test.d.ts.map +1 -0
  39. package/dist/llm/__tests__/toolResultParts.test.d.ts +2 -0
  40. package/dist/llm/__tests__/toolResultParts.test.d.ts.map +1 -0
  41. package/dist/llm/messageFormat.d.ts.map +1 -1
  42. package/dist/llm/ndjson.d.ts +5 -0
  43. package/dist/llm/ndjson.d.ts.map +1 -0
  44. package/dist/llm/providers/anthropic.d.ts +1 -0
  45. package/dist/llm/providers/anthropic.d.ts.map +1 -1
  46. package/dist/llm/providers/google.d.ts +1 -0
  47. package/dist/llm/providers/google.d.ts.map +1 -1
  48. package/dist/llm/providers/index.d.ts.map +1 -1
  49. package/dist/llm/providers/ollama/index.d.ts +18 -0
  50. package/dist/llm/providers/ollama/index.d.ts.map +1 -0
  51. package/dist/llm/providers/ollama/utils.d.ts +7 -0
  52. package/dist/llm/providers/ollama/utils.d.ts.map +1 -0
  53. package/dist/llm/providers/openai.d.ts +1 -0
  54. package/dist/llm/providers/openai.d.ts.map +1 -1
  55. package/dist/llm/toolLoop.d.ts.map +1 -1
  56. package/dist/llm/toolResultParts.d.ts +28 -0
  57. package/dist/llm/toolResultParts.d.ts.map +1 -0
  58. package/dist/llm/types.d.ts +1 -1
  59. package/dist/llm/types.d.ts.map +1 -1
  60. package/dist/server/{chunk-FWTBCKWN.js → chunk-A4VH7AIP.js} +1 -1
  61. package/dist/server/{chunk-POSNI7O3.js → chunk-PJNKFX2R.js} +477 -48
  62. package/dist/server/{chunk-2PI2LIIX.js → chunk-QNKR35CW.js} +1 -1
  63. package/dist/server/cli.js +2 -2
  64. package/dist/server/index.js +3 -3
  65. package/dist/server/middleware.js +3 -3
  66. package/dist/server/server.js +2 -2
  67. package/dist/server/shared-routes.js +2 -2
  68. package/dist/server/shared-utils.d.ts +2 -1
  69. package/dist/server/shared-utils.d.ts.map +1 -1
  70. package/dist/server/shared-utils.js +1 -1
  71. package/dist/web/assets/index-DB2aVJQh.css +2 -0
  72. package/dist/web/assets/{index-B5Pi8EF8.js → index-MGfHvrb0.js} +118 -115
  73. package/dist/web/assets/module-BXKdgN2b.js +4 -0
  74. package/dist/web/index.html +3 -3
  75. package/package.json +5 -2
  76. package/dist/client/components/shared/TabCountBadge.d.ts +0 -9
  77. package/dist/client/components/shared/TabCountBadge.d.ts.map +0 -1
  78. package/dist/web/assets/index-BL3J9vaD.css +0 -2
  79. 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: serializeToolResult(p.toolInvocation.result),
192
+ content: toolResultToContent(result),
63
193
  toolCallId: `call_${mi}_${i}_${p.toolInvocation.toolName}`,
64
194
  toolName: p.toolInvocation.toolName,
65
- toolResult: p.toolInvocation.result
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: typeof m.content === "string" ? m.content : JSON.stringify(m.toolResult ?? m.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
- let response;
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 buildHeaders(config) {
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: typeof m.content === "string" ? m.content : JSON.stringify(m.toolResult ?? m.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* streamChat3(params) {
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: buildHeaders(config),
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 chat3(params) {
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: buildHeaders(config),
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 streamChat4(params) {
1316
+ function streamChat5(params) {
886
1317
  switch (params.config.provider) {
887
1318
  case "openai":
888
1319
  case "openai-compatible":
889
- return streamChat3(params);
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 streamChat3(withOpenRouter(params));
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 chat4(params) {
1333
+ function chat5(params) {
901
1334
  switch (params.config.provider) {
902
1335
  case "openai":
903
1336
  case "openai-compatible":
904
- return chat3(params);
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 chat3(withOpenRouter(params));
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 streamChat4({
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: payloadText,
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 chat4({ config, messages, tools, signal });
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: payloadText,
1463
+ content: toolResultToContent(result),
1035
1464
  toolCallId: tc.id,
1036
1465
  toolName: tc.name,
1037
1466
  toolResult: result,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  registerInspectorRoutes
3
- } from "./chunk-FWTBCKWN.js";
3
+ } from "./chunk-A4VH7AIP.js";
4
4
  import {
5
5
  registerStaticRoutes
6
6
  } from "./chunk-3UU5PBIU.js";