@matthesketh/utopia-ai 0.1.0 → 0.3.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.
@@ -37,16 +37,16 @@ function anthropicAdapter(config) {
37
37
  let client = null;
38
38
  async function getClient() {
39
39
  if (client) return client;
40
- let Anthropic;
40
+ let AnthropicCtor;
41
41
  try {
42
42
  const mod = await import("@anthropic-ai/sdk");
43
- Anthropic = mod.Anthropic ?? mod.default;
43
+ AnthropicCtor = mod.Anthropic ?? mod.default;
44
44
  } catch {
45
45
  throw new Error(
46
46
  '@matthesketh/utopia-ai: "@anthropic-ai/sdk" package is required for the Anthropic adapter. Install it with: npm install @anthropic-ai/sdk'
47
47
  );
48
48
  }
49
- client = new Anthropic({
49
+ client = new AnthropicCtor({
50
50
  apiKey: config.apiKey,
51
51
  ...config.baseURL ? { baseURL: config.baseURL } : {}
52
52
  });
@@ -73,17 +73,22 @@ function anthropicAdapter(config) {
73
73
  body.tool_choice = toAnthropicToolChoice(request.toolChoice);
74
74
  }
75
75
  }
76
- const response = await anthropic.messages.create(body);
77
- const textContent = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
78
- const toolCalls = response.content.filter((b) => b.type === "tool_use").map((b) => ({
79
- id: b.id,
80
- name: b.name,
81
- arguments: b.input ?? {}
82
- }));
76
+ const response = await anthropic.messages.create(
77
+ body
78
+ );
79
+ if (!response.content || !Array.isArray(response.content)) {
80
+ throw new Error("Anthropic returned invalid response: missing content array");
81
+ }
82
+ const contentBlocks = response.content;
83
+ const textContent = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("");
84
+ const toolCalls = contentBlocks.filter((b) => b.type === "tool_use").map((b) => {
85
+ const tu = b;
86
+ return { id: tu.id, name: tu.name, arguments: tu.input ?? {} };
87
+ });
83
88
  return {
84
89
  content: textContent,
85
90
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
86
- finishReason: mapStopReason(response.stop_reason),
91
+ finishReason: mapStopReason(response.stop_reason ?? ""),
87
92
  usage: response.usage ? {
88
93
  promptTokens: response.usage.input_tokens,
89
94
  completionTokens: response.usage.output_tokens,
@@ -113,7 +118,9 @@ function anthropicAdapter(config) {
113
118
  body.tool_choice = toAnthropicToolChoice(request.toolChoice);
114
119
  }
115
120
  }
116
- const stream = anthropic.messages.stream(body);
121
+ const stream = anthropic.messages.stream(
122
+ body
123
+ );
117
124
  let promptTokens = 0;
118
125
  for await (const event of stream) {
119
126
  if (event.type === "message_start" && event.message?.usage) {
@@ -144,7 +151,7 @@ function anthropicAdapter(config) {
144
151
  const outputTokens = event.usage?.output_tokens ?? 0;
145
152
  yield {
146
153
  delta: "",
147
- finishReason: mapStopReason(event.delta.stop_reason),
154
+ finishReason: mapStopReason(event.delta.stop_reason ?? ""),
148
155
  usage: event.usage ? {
149
156
  promptTokens,
150
157
  completionTokens: outputTokens,
@@ -3,16 +3,16 @@ function anthropicAdapter(config) {
3
3
  let client = null;
4
4
  async function getClient() {
5
5
  if (client) return client;
6
- let Anthropic;
6
+ let AnthropicCtor;
7
7
  try {
8
8
  const mod = await import("@anthropic-ai/sdk");
9
- Anthropic = mod.Anthropic ?? mod.default;
9
+ AnthropicCtor = mod.Anthropic ?? mod.default;
10
10
  } catch {
11
11
  throw new Error(
12
12
  '@matthesketh/utopia-ai: "@anthropic-ai/sdk" package is required for the Anthropic adapter. Install it with: npm install @anthropic-ai/sdk'
13
13
  );
14
14
  }
15
- client = new Anthropic({
15
+ client = new AnthropicCtor({
16
16
  apiKey: config.apiKey,
17
17
  ...config.baseURL ? { baseURL: config.baseURL } : {}
18
18
  });
@@ -39,17 +39,22 @@ function anthropicAdapter(config) {
39
39
  body.tool_choice = toAnthropicToolChoice(request.toolChoice);
40
40
  }
41
41
  }
42
- const response = await anthropic.messages.create(body);
43
- const textContent = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
44
- const toolCalls = response.content.filter((b) => b.type === "tool_use").map((b) => ({
45
- id: b.id,
46
- name: b.name,
47
- arguments: b.input ?? {}
48
- }));
42
+ const response = await anthropic.messages.create(
43
+ body
44
+ );
45
+ if (!response.content || !Array.isArray(response.content)) {
46
+ throw new Error("Anthropic returned invalid response: missing content array");
47
+ }
48
+ const contentBlocks = response.content;
49
+ const textContent = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("");
50
+ const toolCalls = contentBlocks.filter((b) => b.type === "tool_use").map((b) => {
51
+ const tu = b;
52
+ return { id: tu.id, name: tu.name, arguments: tu.input ?? {} };
53
+ });
49
54
  return {
50
55
  content: textContent,
51
56
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
52
- finishReason: mapStopReason(response.stop_reason),
57
+ finishReason: mapStopReason(response.stop_reason ?? ""),
53
58
  usage: response.usage ? {
54
59
  promptTokens: response.usage.input_tokens,
55
60
  completionTokens: response.usage.output_tokens,
@@ -79,7 +84,9 @@ function anthropicAdapter(config) {
79
84
  body.tool_choice = toAnthropicToolChoice(request.toolChoice);
80
85
  }
81
86
  }
82
- const stream = anthropic.messages.stream(body);
87
+ const stream = anthropic.messages.stream(
88
+ body
89
+ );
83
90
  let promptTokens = 0;
84
91
  for await (const event of stream) {
85
92
  if (event.type === "message_start" && event.message?.usage) {
@@ -110,7 +117,7 @@ function anthropicAdapter(config) {
110
117
  const outputTokens = event.usage?.output_tokens ?? 0;
111
118
  yield {
112
119
  delta: "",
113
- finishReason: mapStopReason(event.delta.stop_reason),
120
+ finishReason: mapStopReason(event.delta.stop_reason ?? ""),
114
121
  usage: event.usage ? {
115
122
  promptTokens,
116
123
  completionTokens: outputTokens,
@@ -33,6 +33,7 @@ __export(google_exports, {
33
33
  googleAdapter: () => googleAdapter
34
34
  });
35
35
  module.exports = __toCommonJS(google_exports);
36
+ var toolCallCounter = 0;
36
37
  function googleAdapter(config) {
37
38
  let genAI = null;
38
39
  async function getGenAI() {
@@ -72,13 +73,16 @@ function googleAdapter(config) {
72
73
  const response = result.response;
73
74
  const candidate = response.candidates?.[0];
74
75
  const parts = candidate?.content?.parts ?? [];
75
- const textParts = parts.filter((p) => p.text).map((p) => p.text);
76
- const fnCalls = parts.filter((p) => p.functionCall);
77
- const toolCalls = fnCalls.map((p) => ({
78
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
79
- name: p.functionCall.name,
80
- arguments: p.functionCall.args ?? {}
81
- }));
76
+ const textParts = parts.filter((p) => "text" in p && p.text).map((p) => p.text);
77
+ const fnCalls = parts.filter((p) => "functionCall" in p && p.functionCall);
78
+ const toolCalls = fnCalls.map((p) => {
79
+ const fc = p.functionCall;
80
+ return {
81
+ id: `call_${++toolCallCounter}_${Date.now().toString(36)}`,
82
+ name: fc.name,
83
+ arguments: fc.args ?? {}
84
+ };
85
+ });
82
86
  return {
83
87
  content: textParts.join(""),
84
88
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
@@ -112,7 +116,7 @@ function googleAdapter(config) {
112
116
  });
113
117
  for await (const chunk of result.stream) {
114
118
  const parts = chunk.candidates?.[0]?.content?.parts ?? [];
115
- const text = parts.filter((p) => p.text).map((p) => p.text).join("");
119
+ const text = parts.filter((p) => "text" in p && p.text).map((p) => p.text).join("");
116
120
  const finishReason = chunk.candidates?.[0]?.finishReason;
117
121
  yield {
118
122
  delta: text,
@@ -132,9 +136,11 @@ function googleAdapter(config) {
132
136
  });
133
137
  const inputs = Array.isArray(request.input) ? request.input : [request.input];
134
138
  const result = await model.batchEmbedContents({
135
- requests: inputs.map((text) => ({
136
- content: { parts: [{ text }] }
137
- }))
139
+ requests: inputs.map(
140
+ (text) => ({
141
+ content: { parts: [{ text }], role: "user" }
142
+ })
143
+ )
138
144
  });
139
145
  return {
140
146
  embeddings: result.embeddings.map((e) => e.values),
@@ -1,4 +1,5 @@
1
1
  // src/adapters/google.ts
2
+ var toolCallCounter = 0;
2
3
  function googleAdapter(config) {
3
4
  let genAI = null;
4
5
  async function getGenAI() {
@@ -38,13 +39,16 @@ function googleAdapter(config) {
38
39
  const response = result.response;
39
40
  const candidate = response.candidates?.[0];
40
41
  const parts = candidate?.content?.parts ?? [];
41
- const textParts = parts.filter((p) => p.text).map((p) => p.text);
42
- const fnCalls = parts.filter((p) => p.functionCall);
43
- const toolCalls = fnCalls.map((p) => ({
44
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
45
- name: p.functionCall.name,
46
- arguments: p.functionCall.args ?? {}
47
- }));
42
+ const textParts = parts.filter((p) => "text" in p && p.text).map((p) => p.text);
43
+ const fnCalls = parts.filter((p) => "functionCall" in p && p.functionCall);
44
+ const toolCalls = fnCalls.map((p) => {
45
+ const fc = p.functionCall;
46
+ return {
47
+ id: `call_${++toolCallCounter}_${Date.now().toString(36)}`,
48
+ name: fc.name,
49
+ arguments: fc.args ?? {}
50
+ };
51
+ });
48
52
  return {
49
53
  content: textParts.join(""),
50
54
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
@@ -78,7 +82,7 @@ function googleAdapter(config) {
78
82
  });
79
83
  for await (const chunk of result.stream) {
80
84
  const parts = chunk.candidates?.[0]?.content?.parts ?? [];
81
- const text = parts.filter((p) => p.text).map((p) => p.text).join("");
85
+ const text = parts.filter((p) => "text" in p && p.text).map((p) => p.text).join("");
82
86
  const finishReason = chunk.candidates?.[0]?.finishReason;
83
87
  yield {
84
88
  delta: text,
@@ -98,9 +102,11 @@ function googleAdapter(config) {
98
102
  });
99
103
  const inputs = Array.isArray(request.input) ? request.input : [request.input];
100
104
  const result = await model.batchEmbedContents({
101
- requests: inputs.map((text) => ({
102
- content: { parts: [{ text }] }
103
- }))
105
+ requests: inputs.map(
106
+ (text) => ({
107
+ content: { parts: [{ text }], role: "user" }
108
+ })
109
+ )
104
110
  });
105
111
  return {
106
112
  embeddings: result.embeddings.map((e) => e.values),
@@ -23,21 +23,23 @@ __export(ollama_exports, {
23
23
  ollamaAdapter: () => ollamaAdapter
24
24
  });
25
25
  module.exports = __toCommonJS(ollama_exports);
26
+ var ollamaToolCallCounter = 0;
26
27
  function ollamaAdapter(config = {}) {
27
28
  const baseURL = (config.baseURL ?? "http://localhost:11434").replace(/\/$/, "");
28
29
  return {
29
30
  async chat(request) {
30
31
  const model = request.model ?? config.defaultModel ?? "llama3.2";
32
+ const options = {};
33
+ if (request.temperature !== void 0) options.temperature = request.temperature;
34
+ if (request.topP !== void 0) options.top_p = request.topP;
35
+ if (request.maxTokens !== void 0) options.num_predict = request.maxTokens;
36
+ if (request.stop) options.stop = request.stop;
31
37
  const body = {
32
38
  model,
33
39
  messages: toOllamaMessages(request.messages),
34
40
  stream: false,
35
- options: {}
41
+ options
36
42
  };
37
- if (request.temperature !== void 0) body.options.temperature = request.temperature;
38
- if (request.topP !== void 0) body.options.top_p = request.topP;
39
- if (request.maxTokens !== void 0) body.options.num_predict = request.maxTokens;
40
- if (request.stop) body.options.stop = request.stop;
41
43
  if (request.tools?.length) {
42
44
  body.tools = request.tools.map(toOllamaTool);
43
45
  }
@@ -52,13 +54,11 @@ function ollamaAdapter(config = {}) {
52
54
  throw new Error(`Ollama error ${response.status}: ${text}`);
53
55
  }
54
56
  const data = await response.json();
55
- const toolCalls = (data.message?.tool_calls ?? []).map(
56
- (tc, i) => ({
57
- id: `call_${i}`,
58
- name: tc.function.name,
59
- arguments: tc.function.arguments ?? {}
60
- })
61
- );
57
+ const toolCalls = (data.message?.tool_calls ?? []).map((tc) => ({
58
+ id: `call_${++ollamaToolCallCounter}_${Date.now().toString(36)}`,
59
+ name: tc.function.name,
60
+ arguments: tc.function.arguments ?? {}
61
+ }));
62
62
  return {
63
63
  content: data.message?.content ?? "",
64
64
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
@@ -73,16 +73,17 @@ function ollamaAdapter(config = {}) {
73
73
  },
74
74
  async *stream(request) {
75
75
  const model = request.model ?? config.defaultModel ?? "llama3.2";
76
+ const streamOptions = {};
77
+ if (request.temperature !== void 0) streamOptions.temperature = request.temperature;
78
+ if (request.topP !== void 0) streamOptions.top_p = request.topP;
79
+ if (request.maxTokens !== void 0) streamOptions.num_predict = request.maxTokens;
80
+ if (request.stop) streamOptions.stop = request.stop;
76
81
  const body = {
77
82
  model,
78
83
  messages: toOllamaMessages(request.messages),
79
84
  stream: true,
80
- options: {}
85
+ options: streamOptions
81
86
  };
82
- if (request.temperature !== void 0) body.options.temperature = request.temperature;
83
- if (request.topP !== void 0) body.options.top_p = request.topP;
84
- if (request.maxTokens !== void 0) body.options.num_predict = request.maxTokens;
85
- if (request.stop) body.options.stop = request.stop;
86
87
  if (request.tools?.length) {
87
88
  body.tools = request.tools.map(toOllamaTool);
88
89
  }
@@ -96,6 +97,9 @@ function ollamaAdapter(config = {}) {
96
97
  const text = await response.text();
97
98
  throw new Error(`Ollama error ${response.status}: ${text}`);
98
99
  }
100
+ if (!response.body) {
101
+ throw new Error("Response body is null \u2014 streaming not supported");
102
+ }
99
103
  const reader = response.body.getReader();
100
104
  const decoder = new TextDecoder();
101
105
  let buffer = "";
@@ -1,19 +1,21 @@
1
1
  // src/adapters/ollama.ts
2
+ var ollamaToolCallCounter = 0;
2
3
  function ollamaAdapter(config = {}) {
3
4
  const baseURL = (config.baseURL ?? "http://localhost:11434").replace(/\/$/, "");
4
5
  return {
5
6
  async chat(request) {
6
7
  const model = request.model ?? config.defaultModel ?? "llama3.2";
8
+ const options = {};
9
+ if (request.temperature !== void 0) options.temperature = request.temperature;
10
+ if (request.topP !== void 0) options.top_p = request.topP;
11
+ if (request.maxTokens !== void 0) options.num_predict = request.maxTokens;
12
+ if (request.stop) options.stop = request.stop;
7
13
  const body = {
8
14
  model,
9
15
  messages: toOllamaMessages(request.messages),
10
16
  stream: false,
11
- options: {}
17
+ options
12
18
  };
13
- if (request.temperature !== void 0) body.options.temperature = request.temperature;
14
- if (request.topP !== void 0) body.options.top_p = request.topP;
15
- if (request.maxTokens !== void 0) body.options.num_predict = request.maxTokens;
16
- if (request.stop) body.options.stop = request.stop;
17
19
  if (request.tools?.length) {
18
20
  body.tools = request.tools.map(toOllamaTool);
19
21
  }
@@ -28,13 +30,11 @@ function ollamaAdapter(config = {}) {
28
30
  throw new Error(`Ollama error ${response.status}: ${text}`);
29
31
  }
30
32
  const data = await response.json();
31
- const toolCalls = (data.message?.tool_calls ?? []).map(
32
- (tc, i) => ({
33
- id: `call_${i}`,
34
- name: tc.function.name,
35
- arguments: tc.function.arguments ?? {}
36
- })
37
- );
33
+ const toolCalls = (data.message?.tool_calls ?? []).map((tc) => ({
34
+ id: `call_${++ollamaToolCallCounter}_${Date.now().toString(36)}`,
35
+ name: tc.function.name,
36
+ arguments: tc.function.arguments ?? {}
37
+ }));
38
38
  return {
39
39
  content: data.message?.content ?? "",
40
40
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
@@ -49,16 +49,17 @@ function ollamaAdapter(config = {}) {
49
49
  },
50
50
  async *stream(request) {
51
51
  const model = request.model ?? config.defaultModel ?? "llama3.2";
52
+ const streamOptions = {};
53
+ if (request.temperature !== void 0) streamOptions.temperature = request.temperature;
54
+ if (request.topP !== void 0) streamOptions.top_p = request.topP;
55
+ if (request.maxTokens !== void 0) streamOptions.num_predict = request.maxTokens;
56
+ if (request.stop) streamOptions.stop = request.stop;
52
57
  const body = {
53
58
  model,
54
59
  messages: toOllamaMessages(request.messages),
55
60
  stream: true,
56
- options: {}
61
+ options: streamOptions
57
62
  };
58
- if (request.temperature !== void 0) body.options.temperature = request.temperature;
59
- if (request.topP !== void 0) body.options.top_p = request.topP;
60
- if (request.maxTokens !== void 0) body.options.num_predict = request.maxTokens;
61
- if (request.stop) body.options.stop = request.stop;
62
63
  if (request.tools?.length) {
63
64
  body.tools = request.tools.map(toOllamaTool);
64
65
  }
@@ -72,6 +73,9 @@ function ollamaAdapter(config = {}) {
72
73
  const text = await response.text();
73
74
  throw new Error(`Ollama error ${response.status}: ${text}`);
74
75
  }
76
+ if (!response.body) {
77
+ throw new Error("Response body is null \u2014 streaming not supported");
78
+ }
75
79
  const reader = response.body.getReader();
76
80
  const decoder = new TextDecoder();
77
81
  let buffer = "";
@@ -37,16 +37,16 @@ function openaiAdapter(config) {
37
37
  let client = null;
38
38
  async function getClient() {
39
39
  if (client) return client;
40
- let OpenAI;
40
+ let OpenAICtor;
41
41
  try {
42
42
  const mod = await import("openai");
43
- OpenAI = mod.OpenAI ?? mod.default;
43
+ OpenAICtor = mod.OpenAI ?? mod.default;
44
44
  } catch {
45
45
  throw new Error(
46
46
  '@matthesketh/utopia-ai: "openai" package is required for the OpenAI adapter. Install it with: npm install openai'
47
47
  );
48
48
  }
49
- client = new OpenAI({
49
+ client = new OpenAICtor({
50
50
  apiKey: config.apiKey,
51
51
  baseURL: config.baseURL,
52
52
  organization: config.organization
@@ -72,11 +72,20 @@ function openaiAdapter(config) {
72
72
  body.tool_choice = toOpenAIToolChoice(request.toolChoice);
73
73
  }
74
74
  }
75
- const response = await openai.chat.completions.create(body);
75
+ const response = await openai.chat.completions.create(
76
+ body
77
+ );
78
+ if (!response.choices?.length) {
79
+ throw new Error("OpenAI returned empty choices array");
80
+ }
76
81
  const choice = response.choices[0];
77
82
  return {
78
83
  content: choice.message.content ?? "",
79
- toolCalls: choice.message.tool_calls?.map(fromOpenAIToolCall),
84
+ toolCalls: choice.message.tool_calls?.map(
85
+ (tc) => fromOpenAIToolCall(
86
+ tc
87
+ )
88
+ ),
80
89
  finishReason: mapFinishReason(choice.finish_reason),
81
90
  usage: response.usage ? {
82
91
  promptTokens: response.usage.prompt_tokens,
@@ -106,7 +115,9 @@ function openaiAdapter(config) {
106
115
  body.tool_choice = toOpenAIToolChoice(request.toolChoice);
107
116
  }
108
117
  }
109
- const stream = await openai.chat.completions.create(body);
118
+ const stream = await openai.chat.completions.create(
119
+ body
120
+ );
110
121
  for await (const chunk of stream) {
111
122
  const delta = chunk.choices?.[0]?.delta;
112
123
  const finishReason = chunk.choices?.[0]?.finish_reason;
@@ -218,7 +229,7 @@ function toOpenAIToolChoice(choice) {
218
229
  if (choice && typeof choice === "object" && "name" in choice) {
219
230
  return { type: "function", function: { name: choice.name } };
220
231
  }
221
- return choice;
232
+ return void 0;
222
233
  }
223
234
  function fromOpenAIToolCall(tc) {
224
235
  return {
@@ -3,16 +3,16 @@ function openaiAdapter(config) {
3
3
  let client = null;
4
4
  async function getClient() {
5
5
  if (client) return client;
6
- let OpenAI;
6
+ let OpenAICtor;
7
7
  try {
8
8
  const mod = await import("openai");
9
- OpenAI = mod.OpenAI ?? mod.default;
9
+ OpenAICtor = mod.OpenAI ?? mod.default;
10
10
  } catch {
11
11
  throw new Error(
12
12
  '@matthesketh/utopia-ai: "openai" package is required for the OpenAI adapter. Install it with: npm install openai'
13
13
  );
14
14
  }
15
- client = new OpenAI({
15
+ client = new OpenAICtor({
16
16
  apiKey: config.apiKey,
17
17
  baseURL: config.baseURL,
18
18
  organization: config.organization
@@ -38,11 +38,20 @@ function openaiAdapter(config) {
38
38
  body.tool_choice = toOpenAIToolChoice(request.toolChoice);
39
39
  }
40
40
  }
41
- const response = await openai.chat.completions.create(body);
41
+ const response = await openai.chat.completions.create(
42
+ body
43
+ );
44
+ if (!response.choices?.length) {
45
+ throw new Error("OpenAI returned empty choices array");
46
+ }
42
47
  const choice = response.choices[0];
43
48
  return {
44
49
  content: choice.message.content ?? "",
45
- toolCalls: choice.message.tool_calls?.map(fromOpenAIToolCall),
50
+ toolCalls: choice.message.tool_calls?.map(
51
+ (tc) => fromOpenAIToolCall(
52
+ tc
53
+ )
54
+ ),
46
55
  finishReason: mapFinishReason(choice.finish_reason),
47
56
  usage: response.usage ? {
48
57
  promptTokens: response.usage.prompt_tokens,
@@ -72,7 +81,9 @@ function openaiAdapter(config) {
72
81
  body.tool_choice = toOpenAIToolChoice(request.toolChoice);
73
82
  }
74
83
  }
75
- const stream = await openai.chat.completions.create(body);
84
+ const stream = await openai.chat.completions.create(
85
+ body
86
+ );
76
87
  for await (const chunk of stream) {
77
88
  const delta = chunk.choices?.[0]?.delta;
78
89
  const finishReason = chunk.choices?.[0]?.finish_reason;
@@ -184,7 +195,7 @@ function toOpenAIToolChoice(choice) {
184
195
  if (choice && typeof choice === "object" && "name" in choice) {
185
196
  return { type: "function", function: { name: choice.name } };
186
197
  }
187
- return choice;
198
+ return void 0;
188
199
  }
189
200
  function fromOpenAIToolCall(tc) {
190
201
  return {
package/dist/index.cjs CHANGED
@@ -44,7 +44,10 @@ function createAI(adapter, options) {
44
44
  }
45
45
  return response;
46
46
  } catch (err) {
47
- hooks?.onError?.(err, { method: "chat", request: req });
47
+ hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
48
+ method: "chat",
49
+ request: req
50
+ });
48
51
  throw err;
49
52
  }
50
53
  },
@@ -58,7 +61,10 @@ function createAI(adapter, options) {
58
61
  try {
59
62
  yield* source.call(adapter, req);
60
63
  } catch (err) {
61
- hooks?.onError?.(err, { method: "stream", request: req });
64
+ hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
65
+ method: "stream",
66
+ request: req
67
+ });
62
68
  throw err;
63
69
  }
64
70
  };
@@ -71,12 +77,7 @@ function createAI(adapter, options) {
71
77
  return adapter.embeddings(request);
72
78
  },
73
79
  async run(options2) {
74
- const {
75
- tools,
76
- maxRounds = 10,
77
- onToolCall,
78
- ...requestBase
79
- } = options2;
80
+ const { tools, maxRounds = 10, onToolCall, ...requestBase } = options2;
80
81
  const messages = [...options2.messages];
81
82
  const toolDefs = tools.map((t) => t.definition);
82
83
  const handlerMap = new Map(tools.map((t) => [t.definition.name, t.handler]));
@@ -95,7 +96,10 @@ function createAI(adapter, options) {
95
96
  try {
96
97
  response = await withRetry(() => adapter.chat(req2), retry);
97
98
  } catch (err) {
98
- hooks?.onError?.(err, { method: "run", request: req2 });
99
+ hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
100
+ method: "run",
101
+ request: req2
102
+ });
99
103
  throw err;
100
104
  }
101
105
  if (hooks?.onAfterChat) {
@@ -124,19 +128,21 @@ function createAI(adapter, options) {
124
128
  try {
125
129
  result = await handler(call.arguments);
126
130
  } catch (err) {
127
- result = err.message ?? String(err);
131
+ result = err instanceof Error ? err.message : String(err);
128
132
  isError = true;
129
133
  }
130
134
  }
131
135
  onToolCall?.(call, result);
132
136
  messages.push({
133
137
  role: "tool",
134
- content: [{
135
- type: "tool_result",
136
- id: call.id,
137
- content: typeof result === "string" ? result : JSON.stringify(result),
138
- isError
139
- }]
138
+ content: [
139
+ {
140
+ type: "tool_result",
141
+ id: call.id,
142
+ content: typeof result === "string" ? result : JSON.stringify(result),
143
+ isError
144
+ }
145
+ ]
140
146
  });
141
147
  }
142
148
  }
@@ -154,7 +160,10 @@ function createAI(adapter, options) {
154
160
  try {
155
161
  finalResponse = await withRetry(() => adapter.chat(req), retry);
156
162
  } catch (err) {
157
- hooks?.onError?.(err, { method: "run", request: req });
163
+ hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
164
+ method: "run",
165
+ request: req
166
+ });
158
167
  throw err;
159
168
  }
160
169
  if (hooks?.onAfterChat) {
@@ -173,8 +182,8 @@ async function withRetry(fn, config) {
173
182
  try {
174
183
  return await fn();
175
184
  } catch (err) {
176
- lastError = err;
177
- if (attempt < maxRetries && shouldRetry(err)) {
185
+ lastError = err instanceof Error ? err : new Error(String(err));
186
+ if (attempt < maxRetries && shouldRetry(lastError)) {
178
187
  await sleep(baseDelay * Math.pow(2, attempt));
179
188
  continue;
180
189
  }
@@ -204,7 +213,7 @@ async function streamSSE(res, stream, options) {
204
213
  res.writeHead(200, {
205
214
  "Content-Type": "text/event-stream",
206
215
  "Cache-Control": "no-cache",
207
- "Connection": "keep-alive",
216
+ Connection: "keep-alive",
208
217
  "X-Accel-Buffering": "no"
209
218
  });
210
219
  try {
@@ -227,6 +236,9 @@ async function collectStream(stream) {
227
236
  return result;
228
237
  }
229
238
  async function* parseSSEStream(response) {
239
+ if (!response.body) {
240
+ throw new Error("Response body is null \u2014 streaming not supported");
241
+ }
230
242
  const reader = response.body.getReader();
231
243
  const decoder = new TextDecoder();
232
244
  let buffer = "";
package/dist/index.js CHANGED
@@ -15,7 +15,10 @@ function createAI(adapter, options) {
15
15
  }
16
16
  return response;
17
17
  } catch (err) {
18
- hooks?.onError?.(err, { method: "chat", request: req });
18
+ hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
19
+ method: "chat",
20
+ request: req
21
+ });
19
22
  throw err;
20
23
  }
21
24
  },
@@ -29,7 +32,10 @@ function createAI(adapter, options) {
29
32
  try {
30
33
  yield* source.call(adapter, req);
31
34
  } catch (err) {
32
- hooks?.onError?.(err, { method: "stream", request: req });
35
+ hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
36
+ method: "stream",
37
+ request: req
38
+ });
33
39
  throw err;
34
40
  }
35
41
  };
@@ -42,12 +48,7 @@ function createAI(adapter, options) {
42
48
  return adapter.embeddings(request);
43
49
  },
44
50
  async run(options2) {
45
- const {
46
- tools,
47
- maxRounds = 10,
48
- onToolCall,
49
- ...requestBase
50
- } = options2;
51
+ const { tools, maxRounds = 10, onToolCall, ...requestBase } = options2;
51
52
  const messages = [...options2.messages];
52
53
  const toolDefs = tools.map((t) => t.definition);
53
54
  const handlerMap = new Map(tools.map((t) => [t.definition.name, t.handler]));
@@ -66,7 +67,10 @@ function createAI(adapter, options) {
66
67
  try {
67
68
  response = await withRetry(() => adapter.chat(req2), retry);
68
69
  } catch (err) {
69
- hooks?.onError?.(err, { method: "run", request: req2 });
70
+ hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
71
+ method: "run",
72
+ request: req2
73
+ });
70
74
  throw err;
71
75
  }
72
76
  if (hooks?.onAfterChat) {
@@ -95,19 +99,21 @@ function createAI(adapter, options) {
95
99
  try {
96
100
  result = await handler(call.arguments);
97
101
  } catch (err) {
98
- result = err.message ?? String(err);
102
+ result = err instanceof Error ? err.message : String(err);
99
103
  isError = true;
100
104
  }
101
105
  }
102
106
  onToolCall?.(call, result);
103
107
  messages.push({
104
108
  role: "tool",
105
- content: [{
106
- type: "tool_result",
107
- id: call.id,
108
- content: typeof result === "string" ? result : JSON.stringify(result),
109
- isError
110
- }]
109
+ content: [
110
+ {
111
+ type: "tool_result",
112
+ id: call.id,
113
+ content: typeof result === "string" ? result : JSON.stringify(result),
114
+ isError
115
+ }
116
+ ]
111
117
  });
112
118
  }
113
119
  }
@@ -125,7 +131,10 @@ function createAI(adapter, options) {
125
131
  try {
126
132
  finalResponse = await withRetry(() => adapter.chat(req), retry);
127
133
  } catch (err) {
128
- hooks?.onError?.(err, { method: "run", request: req });
134
+ hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
135
+ method: "run",
136
+ request: req
137
+ });
129
138
  throw err;
130
139
  }
131
140
  if (hooks?.onAfterChat) {
@@ -144,8 +153,8 @@ async function withRetry(fn, config) {
144
153
  try {
145
154
  return await fn();
146
155
  } catch (err) {
147
- lastError = err;
148
- if (attempt < maxRetries && shouldRetry(err)) {
156
+ lastError = err instanceof Error ? err : new Error(String(err));
157
+ if (attempt < maxRetries && shouldRetry(lastError)) {
149
158
  await sleep(baseDelay * Math.pow(2, attempt));
150
159
  continue;
151
160
  }
@@ -175,7 +184,7 @@ async function streamSSE(res, stream, options) {
175
184
  res.writeHead(200, {
176
185
  "Content-Type": "text/event-stream",
177
186
  "Cache-Control": "no-cache",
178
- "Connection": "keep-alive",
187
+ Connection: "keep-alive",
179
188
  "X-Accel-Buffering": "no"
180
189
  });
181
190
  try {
@@ -198,6 +207,9 @@ async function collectStream(stream) {
198
207
  return result;
199
208
  }
200
209
  async function* parseSSEStream(response) {
210
+ if (!response.body) {
211
+ throw new Error("Response body is null \u2014 streaming not supported");
212
+ }
201
213
  const reader = response.body.getReader();
202
214
  const decoder = new TextDecoder();
203
215
  let buffer = "";
@@ -47,13 +47,14 @@ function createMCPServer(config) {
47
47
  const result = await dispatch(request);
48
48
  return { jsonrpc: "2.0", id: request.id, result };
49
49
  } catch (err) {
50
+ const rpcErr = err;
50
51
  return {
51
52
  jsonrpc: "2.0",
52
53
  id: request.id,
53
54
  error: {
54
- code: err.code ?? -32603,
55
- message: err.message ?? "Internal error",
56
- data: err.data
55
+ code: rpcErr.code ?? -32603,
56
+ message: rpcErr.message ?? "Internal error",
57
+ data: rpcErr.data
57
58
  }
58
59
  };
59
60
  }
@@ -172,29 +173,28 @@ function createMCPClient(config) {
172
173
  }
173
174
  const result = await response.json();
174
175
  if (result.error) {
175
- const err = new Error(result.error.message);
176
- err.code = result.error.code;
177
- err.data = result.error.data;
176
+ const err = Object.assign(new Error(result.error.message), {
177
+ code: result.error.code,
178
+ data: result.error.data
179
+ });
178
180
  throw err;
179
181
  }
180
182
  return result.result;
181
183
  }
182
184
  return {
183
185
  async initialize() {
184
- const result = await rpc("initialize", {
186
+ return rpc("initialize", {
185
187
  protocolVersion: "2024-11-05",
186
188
  capabilities: {},
187
189
  clientInfo: { name: "utopia-mcp-client", version: "1.0.0" }
188
190
  });
189
- return result;
190
191
  },
191
192
  async listTools() {
192
193
  const result = await rpc("tools/list");
193
194
  return result.tools ?? [];
194
195
  },
195
196
  async callTool(name, args) {
196
- const result = await rpc("tools/call", { name, arguments: args });
197
- return result;
197
+ return rpc("tools/call", { name, arguments: args });
198
198
  },
199
199
  async listResources() {
200
200
  const result = await rpc("resources/list");
@@ -209,27 +209,28 @@ function createMCPClient(config) {
209
209
  return result.prompts ?? [];
210
210
  },
211
211
  async getPrompt(name, args) {
212
- const result = await rpc("prompts/get", { name, arguments: args });
213
- return result;
212
+ return rpc("prompts/get", { name, arguments: args });
214
213
  },
215
214
  async toToolHandlers() {
216
215
  const tools = await this.listTools();
217
- return tools.map((tool) => ({
218
- definition: {
219
- name: tool.name,
220
- description: tool.description,
221
- parameters: tool.inputSchema
222
- },
223
- handler: async (args) => {
224
- const result = await this.callTool(tool.name, args);
225
- if (result.isError) {
226
- throw new Error(
227
- result.content.map((c) => c.text ?? "").join("\n") || "Tool call failed"
228
- );
216
+ return tools.map(
217
+ (tool) => ({
218
+ definition: {
219
+ name: tool.name,
220
+ description: tool.description,
221
+ parameters: tool.inputSchema
222
+ },
223
+ handler: async (args) => {
224
+ const result = await this.callTool(tool.name, args);
225
+ if (result.isError) {
226
+ throw new Error(
227
+ result.content.map((c) => c.text ?? "").join("\n") || "Tool call failed"
228
+ );
229
+ }
230
+ return result.content.map((c) => c.text ?? "").join("\n");
229
231
  }
230
- return result.content.map((c) => c.text ?? "").join("\n");
231
- }
232
- }));
232
+ })
233
+ );
233
234
  }
234
235
  };
235
236
  }
@@ -267,18 +268,24 @@ async function handlePost(server, req, res) {
267
268
  res.end(JSON.stringify(response));
268
269
  } catch (err) {
269
270
  res.writeHead(400, { "Content-Type": "application/json" });
270
- res.end(JSON.stringify({
271
- jsonrpc: "2.0",
272
- id: null,
273
- error: { code: -32700, message: "Parse error", data: err.message }
274
- }));
271
+ res.end(
272
+ JSON.stringify({
273
+ jsonrpc: "2.0",
274
+ id: null,
275
+ error: {
276
+ code: -32700,
277
+ message: "Parse error",
278
+ data: err instanceof Error ? err.message : String(err)
279
+ }
280
+ })
281
+ );
275
282
  }
276
283
  }
277
284
  function handleSSE(server, _req, res) {
278
285
  res.writeHead(200, {
279
286
  "Content-Type": "text/event-stream",
280
287
  "Cache-Control": "no-cache",
281
- "Connection": "keep-alive"
288
+ Connection: "keep-alive"
282
289
  });
283
290
  const endpointUrl = "./";
284
291
  res.write(`event: endpoint
@@ -170,27 +170,6 @@ interface MCPClient {
170
170
  */
171
171
  toToolHandlers(): Promise<ToolHandler[]>;
172
172
  }
173
- /**
174
- * Create an MCP client that connects to an MCP server over HTTP.
175
- *
176
- * ```ts
177
- * import { createMCPClient } from '@matthesketh/utopia-ai/mcp';
178
- *
179
- * const client = createMCPClient({ url: 'http://localhost:3001/mcp' });
180
- * await client.initialize();
181
- *
182
- * const tools = await client.listTools();
183
- * const result = await client.callTool('get_weather', { city: 'NYC' });
184
- *
185
- * // Bridge MCP tools into AI tool loop
186
- * const ai = createAI(openaiAdapter({ apiKey: '...' }));
187
- * const toolHandlers = await client.toToolHandlers();
188
- * const response = await ai.run({
189
- * messages: [{ role: 'user', content: 'What is the weather?' }],
190
- * tools: toolHandlers,
191
- * });
192
- * ```
193
- */
194
173
  declare function createMCPClient(config: MCPClientConfig): MCPClient;
195
174
 
196
175
  /**
@@ -170,27 +170,6 @@ interface MCPClient {
170
170
  */
171
171
  toToolHandlers(): Promise<ToolHandler[]>;
172
172
  }
173
- /**
174
- * Create an MCP client that connects to an MCP server over HTTP.
175
- *
176
- * ```ts
177
- * import { createMCPClient } from '@matthesketh/utopia-ai/mcp';
178
- *
179
- * const client = createMCPClient({ url: 'http://localhost:3001/mcp' });
180
- * await client.initialize();
181
- *
182
- * const tools = await client.listTools();
183
- * const result = await client.callTool('get_weather', { city: 'NYC' });
184
- *
185
- * // Bridge MCP tools into AI tool loop
186
- * const ai = createAI(openaiAdapter({ apiKey: '...' }));
187
- * const toolHandlers = await client.toToolHandlers();
188
- * const response = await ai.run({
189
- * messages: [{ role: 'user', content: 'What is the weather?' }],
190
- * tools: toolHandlers,
191
- * });
192
- * ```
193
- */
194
173
  declare function createMCPClient(config: MCPClientConfig): MCPClient;
195
174
 
196
175
  /**
package/dist/mcp/index.js CHANGED
@@ -19,13 +19,14 @@ function createMCPServer(config) {
19
19
  const result = await dispatch(request);
20
20
  return { jsonrpc: "2.0", id: request.id, result };
21
21
  } catch (err) {
22
+ const rpcErr = err;
22
23
  return {
23
24
  jsonrpc: "2.0",
24
25
  id: request.id,
25
26
  error: {
26
- code: err.code ?? -32603,
27
- message: err.message ?? "Internal error",
28
- data: err.data
27
+ code: rpcErr.code ?? -32603,
28
+ message: rpcErr.message ?? "Internal error",
29
+ data: rpcErr.data
29
30
  }
30
31
  };
31
32
  }
@@ -144,29 +145,28 @@ function createMCPClient(config) {
144
145
  }
145
146
  const result = await response.json();
146
147
  if (result.error) {
147
- const err = new Error(result.error.message);
148
- err.code = result.error.code;
149
- err.data = result.error.data;
148
+ const err = Object.assign(new Error(result.error.message), {
149
+ code: result.error.code,
150
+ data: result.error.data
151
+ });
150
152
  throw err;
151
153
  }
152
154
  return result.result;
153
155
  }
154
156
  return {
155
157
  async initialize() {
156
- const result = await rpc("initialize", {
158
+ return rpc("initialize", {
157
159
  protocolVersion: "2024-11-05",
158
160
  capabilities: {},
159
161
  clientInfo: { name: "utopia-mcp-client", version: "1.0.0" }
160
162
  });
161
- return result;
162
163
  },
163
164
  async listTools() {
164
165
  const result = await rpc("tools/list");
165
166
  return result.tools ?? [];
166
167
  },
167
168
  async callTool(name, args) {
168
- const result = await rpc("tools/call", { name, arguments: args });
169
- return result;
169
+ return rpc("tools/call", { name, arguments: args });
170
170
  },
171
171
  async listResources() {
172
172
  const result = await rpc("resources/list");
@@ -181,27 +181,28 @@ function createMCPClient(config) {
181
181
  return result.prompts ?? [];
182
182
  },
183
183
  async getPrompt(name, args) {
184
- const result = await rpc("prompts/get", { name, arguments: args });
185
- return result;
184
+ return rpc("prompts/get", { name, arguments: args });
186
185
  },
187
186
  async toToolHandlers() {
188
187
  const tools = await this.listTools();
189
- return tools.map((tool) => ({
190
- definition: {
191
- name: tool.name,
192
- description: tool.description,
193
- parameters: tool.inputSchema
194
- },
195
- handler: async (args) => {
196
- const result = await this.callTool(tool.name, args);
197
- if (result.isError) {
198
- throw new Error(
199
- result.content.map((c) => c.text ?? "").join("\n") || "Tool call failed"
200
- );
188
+ return tools.map(
189
+ (tool) => ({
190
+ definition: {
191
+ name: tool.name,
192
+ description: tool.description,
193
+ parameters: tool.inputSchema
194
+ },
195
+ handler: async (args) => {
196
+ const result = await this.callTool(tool.name, args);
197
+ if (result.isError) {
198
+ throw new Error(
199
+ result.content.map((c) => c.text ?? "").join("\n") || "Tool call failed"
200
+ );
201
+ }
202
+ return result.content.map((c) => c.text ?? "").join("\n");
201
203
  }
202
- return result.content.map((c) => c.text ?? "").join("\n");
203
- }
204
- }));
204
+ })
205
+ );
205
206
  }
206
207
  };
207
208
  }
@@ -239,18 +240,24 @@ async function handlePost(server, req, res) {
239
240
  res.end(JSON.stringify(response));
240
241
  } catch (err) {
241
242
  res.writeHead(400, { "Content-Type": "application/json" });
242
- res.end(JSON.stringify({
243
- jsonrpc: "2.0",
244
- id: null,
245
- error: { code: -32700, message: "Parse error", data: err.message }
246
- }));
243
+ res.end(
244
+ JSON.stringify({
245
+ jsonrpc: "2.0",
246
+ id: null,
247
+ error: {
248
+ code: -32700,
249
+ message: "Parse error",
250
+ data: err instanceof Error ? err.message : String(err)
251
+ }
252
+ })
253
+ );
247
254
  }
248
255
  }
249
256
  function handleSSE(server, _req, res) {
250
257
  res.writeHead(200, {
251
258
  "Content-Type": "text/event-stream",
252
259
  "Cache-Control": "no-cache",
253
- "Connection": "keep-alive"
260
+ Connection: "keep-alive"
254
261
  });
255
262
  const endpointUrl = "./";
256
263
  res.write(`event: endpoint
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matthesketh/utopia-ai",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "AI adapters and MCP support for UtopiaJS",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -67,9 +67,9 @@
67
67
  "dist"
68
68
  ],
69
69
  "peerDependencies": {
70
- "@anthropic-ai/sdk": "^0.30.0",
71
- "@google/generative-ai": "^0.21.0",
72
- "openai": "^4.0.0"
70
+ "@anthropic-ai/sdk": "^0.30.0 || ^0.74.0",
71
+ "@google/generative-ai": "^0.21.0 || ^0.24.0",
72
+ "openai": "^4.0.0 || ^5.0.0 || ^6.0.0"
73
73
  },
74
74
  "peerDependenciesMeta": {
75
75
  "openai": {