@sisu-ai/adapter-ollama 9.0.1 → 9.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,9 @@
1
+ <div align="right">
2
+ <a href="https://github.com/finger-gun/sisu"><img src="https://github.com/finger-gun/sisu/raw/main/sisu-light.svg" alt="ProjectSpecs" width="100" /></a>
3
+ </div>
4
+
5
+ ---
6
+
1
7
  # @sisu-ai/adapter-ollama
2
8
 
3
9
  Ollama Chat adapter with native tools support.
@@ -116,3 +122,78 @@ Discover what you can do through examples or documentation. Check it out at http
116
122
  - [License](https://github.com/finger-gun/sisu/blob/main/LICENSE)
117
123
  - [Report a Bug](https://github.com/finger-gun/sisu/issues/new?template=bug_report.md)
118
124
  - [Request a Feature](https://github.com/finger-gun/sisu/issues/new?template=feature_request.md)
125
+
126
+ ---
127
+
128
+ ## Documentation
129
+
130
+ **Core** — [Package docs](packages/core/README.md) · [Error types](packages/core/ERROR_TYPES.md)
131
+
132
+ **Adapters** — [OpenAI](packages/adapters/openai/README.md) · [Anthropic](packages/adapters/anthropic/README.md) · [Ollama](packages/adapters/ollama/README.md)
133
+
134
+ <details>
135
+ <summary>All middleware packages</summary>
136
+
137
+ - [@sisu-ai/mw-agent-run-api](packages/middleware/agent-run-api/README.md)
138
+ - [@sisu-ai/mw-context-compressor](packages/middleware/context-compressor/README.md)
139
+ - [@sisu-ai/mw-control-flow](packages/middleware/control-flow/README.md)
140
+ - [@sisu-ai/mw-conversation-buffer](packages/middleware/conversation-buffer/README.md)
141
+ - [@sisu-ai/mw-cors](packages/middleware/cors/README.md)
142
+ - [@sisu-ai/mw-error-boundary](packages/middleware/error-boundary/README.md)
143
+ - [@sisu-ai/mw-guardrails](packages/middleware/guardrails/README.md)
144
+ - [@sisu-ai/mw-invariants](packages/middleware/invariants/README.md)
145
+ - [@sisu-ai/mw-orchestration](packages/middleware/orchestration/README.md)
146
+ - [@sisu-ai/mw-rag](packages/middleware/rag/README.md)
147
+ - [@sisu-ai/mw-react-parser](packages/middleware/react-parser/README.md)
148
+ - [@sisu-ai/mw-register-tools](packages/middleware/register-tools/README.md)
149
+ - [@sisu-ai/mw-tool-calling](packages/middleware/tool-calling/README.md)
150
+ - [@sisu-ai/mw-trace-viewer](packages/middleware/trace-viewer/README.md)
151
+ - [@sisu-ai/mw-usage-tracker](packages/middleware/usage-tracker/README.md)
152
+ </details>
153
+
154
+ <details>
155
+ <summary>All tool packages</summary>
156
+
157
+ - [@sisu-ai/tool-aws-s3](packages/tools/aws-s3/README.md)
158
+ - [@sisu-ai/tool-azure-blob](packages/tools/azure-blob/README.md)
159
+ - [@sisu-ai/tool-extract-urls](packages/tools/extract-urls/README.md)
160
+ - [@sisu-ai/tool-github-projects](packages/tools/github-projects/README.md)
161
+ - [@sisu-ai/tool-summarize-text](packages/tools/summarize-text/README.md)
162
+ - [@sisu-ai/tool-terminal](packages/tools/terminal/README.md)
163
+ - [@sisu-ai/tool-vec-chroma](packages/tools/vec-chroma/README.md)
164
+ - [@sisu-ai/tool-web-fetch](packages/tools/web-fetch/README.md)
165
+ - [@sisu-ai/tool-web-search-duckduckgo](packages/tools/web-search-duckduckgo/README.md)
166
+ - [@sisu-ai/tool-web-search-google](packages/tools/web-search-google/README.md)
167
+ - [@sisu-ai/tool-web-search-openai](packages/tools/web-search-openai/README.md)
168
+ - [@sisu-ai/tool-wikipedia](packages/tools/wikipedia/README.md)
169
+ </details>
170
+
171
+ <details>
172
+ <summary>All examples</summary>
173
+
174
+ **Anthropic** — [hello](examples/anthropic-hello/README.md) · [control-flow](examples/anthropic-control-flow/README.md) · [stream](examples/anthropic-stream/README.md) · [weather](examples/anthropic-weather/README.md)
175
+
176
+ **Ollama** — [hello](examples/ollama-hello/README.md) · [stream](examples/ollama-stream/README.md) · [vision](examples/ollama-vision/README.md) · [weather](examples/ollama-weather/README.md) · [web-search](examples/ollama-web-search/README.md)
177
+
178
+ **OpenAI** — [hello](examples/openai-hello/README.md) · [weather](examples/openai-weather/README.md) · [stream](examples/openai-stream/README.md) · [vision](examples/openai-vision/README.md) · [reasoning](examples/openai-reasoning/README.md) · [react](examples/openai-react/README.md) · [control-flow](examples/openai-control-flow/README.md) · [branch](examples/openai-branch/README.md) · [parallel](examples/openai-parallel/README.md) · [graph](examples/openai-graph/README.md) · [orchestration](examples/openai-orchestration/README.md) · [orchestration-adaptive](examples/openai-orchestration-adaptive/README.md) · [guardrails](examples/openai-guardrails/README.md) · [error-handling](examples/openai-error-handling/README.md) · [rag-chroma](examples/openai-rag-chroma/README.md) · [web-search](examples/openai-web-search/README.md) · [web-fetch](examples/openai-web-fetch/README.md) · [wikipedia](examples/openai-wikipedia/README.md) · [terminal](examples/openai-terminal/README.md) · [github-projects](examples/openai-github-projects/README.md) · [server](examples/openai-server/README.md) · [aws-s3](examples/openai-aws-s3/README.md) · [azure-blob](examples/openai-azure-blob/README.md)
179
+ </details>
180
+
181
+ ---
182
+
183
+ ## Contributing
184
+
185
+ We build Sisu in the open. Contributions welcome.
186
+
187
+ [Contributing Guide](CONTRIBUTING.md) · [Report a Bug](https://github.com/finger-gun/sisu/issues/new?template=bug_report.md) · [Request a Feature](https://github.com/finger-gun/sisu/issues/new?template=feature_request.md) · [Code of Conduct](CODE_OF_CONDUCT.md)
188
+
189
+ ---
190
+
191
+ <div align="center">
192
+
193
+ **[Star on GitHub](https://github.com/finger-gun/sisu)** if Sisu helps you build better agents.
194
+
195
+ *Quiet, determined, relentlessly useful.*
196
+
197
+ [Apache 2.0 License](LICENSE)
198
+
199
+ </div>
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { LLM } from '@sisu-ai/core';
1
+ import type { LLM } from "@sisu-ai/core";
2
2
  export interface OllamaAdapterOptions {
3
3
  model: string;
4
4
  baseUrl?: string;
package/dist/index.js CHANGED
@@ -1,24 +1,29 @@
1
- import { firstConfigValue } from '@sisu-ai/core';
1
+ import { firstConfigValue } from "@sisu-ai/core";
2
2
  export function ollamaAdapter(opts) {
3
- const envBase = firstConfigValue(['OLLAMA_BASE_URL', 'BASE_URL']);
4
- const baseUrl = (opts.baseUrl ?? envBase ?? 'http://localhost:11434').replace(/\/$/, '');
3
+ const envBase = firstConfigValue(["OLLAMA_BASE_URL", "BASE_URL"]);
4
+ const baseUrl = (opts.baseUrl ?? envBase ?? "http://localhost:11434").replace(/\/$/, "");
5
5
  const modelName = `ollama:${opts.model}`;
6
- function generate(messages, genOpts) {
6
+ const generate = ((messages, genOpts) => {
7
7
  // Map messages to Ollama format; include assistant tool_calls and tool messages
8
8
  async function mapMessagesWithImages() {
9
9
  const out = [];
10
10
  for (const m of messages) {
11
11
  const base = { role: m.role };
12
12
  const anyM = m;
13
- if (m.role === 'assistant' && Array.isArray(anyM.tool_calls)) {
14
- base.tool_calls = anyM.tool_calls.map((tc) => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: (tc.arguments ?? {}) } }));
13
+ if (m.role === "assistant" && Array.isArray(anyM.tool_calls)) {
14
+ base.tool_calls = anyM.tool_calls.map((tc) => ({
15
+ id: tc.id,
16
+ type: "function",
17
+ function: { name: tc.name, arguments: tc.arguments ?? {} },
18
+ }));
15
19
  const ti = buildTextAndImages(anyM);
16
- base.content = ti.content ?? (m.content !== undefined ? m.content : null);
20
+ base.content =
21
+ ti.content ?? (m.content !== undefined ? m.content : null);
17
22
  if (ti.images?.length)
18
23
  base.images = await toBase64Images(ti.images);
19
24
  }
20
- else if (m.role === 'tool') {
21
- base.content = String(m.content ?? '');
25
+ else if (m.role === "tool") {
26
+ base.content = String(m.content ?? "");
22
27
  if (m.tool_call_id)
23
28
  base.tool_call_id = m.tool_call_id;
24
29
  if (m.name && !m.tool_call_id)
@@ -26,7 +31,7 @@ export function ollamaAdapter(opts) {
26
31
  }
27
32
  else {
28
33
  const ti = buildTextAndImages(anyM);
29
- base.content = ti.content ?? (m.content ?? '');
34
+ base.content = ti.content ?? m.content ?? "";
30
35
  if (ti.images?.length)
31
36
  base.images = await toBase64Images(ti.images);
32
37
  if (m.name)
@@ -40,14 +45,17 @@ export function ollamaAdapter(opts) {
40
45
  return (async function* () {
41
46
  const toolsParam = (genOpts?.tools ?? []).map(toOllamaTool);
42
47
  const mapped = await mapMessagesWithImages();
43
- const baseBody = { model: opts.model, messages: mapped };
48
+ const baseBody = {
49
+ model: opts.model,
50
+ messages: mapped,
51
+ };
44
52
  if (toolsParam.length)
45
53
  baseBody.tools = toolsParam;
46
54
  const res = await fetch(`${baseUrl}/api/chat`, {
47
- method: 'POST',
55
+ method: "POST",
48
56
  headers: {
49
- 'Content-Type': 'application/json',
50
- Accept: 'application/json',
57
+ "Content-Type": "application/json",
58
+ Accept: "application/json",
51
59
  ...(opts.headers ?? {}),
52
60
  },
53
61
  body: JSON.stringify({ ...baseBody, stream: true }),
@@ -57,30 +65,33 @@ export function ollamaAdapter(opts) {
57
65
  throw new Error(`Ollama API error: ${res.status} ${res.statusText} — ${String(err).slice(0, 500)}`);
58
66
  }
59
67
  const decoder = new TextDecoder();
60
- let buf = '';
61
- let full = '';
68
+ let buf = "";
69
+ let full = "";
62
70
  for await (const chunk of res.body) {
63
- const piece = typeof chunk === 'string' ? chunk : decoder.decode(chunk);
71
+ const piece = typeof chunk === "string" ? chunk : decoder.decode(chunk);
64
72
  buf += piece;
65
- const lines = buf.split('\n');
66
- buf = lines.pop() ?? '';
73
+ const lines = buf.split("\n");
74
+ buf = lines.pop() ?? "";
67
75
  for (const line of lines) {
68
76
  if (!line.trim())
69
77
  continue;
70
78
  try {
71
79
  const j = JSON.parse(line);
72
80
  if (j.done) {
73
- yield { type: 'assistant_message', message: { role: 'assistant', content: full } };
81
+ yield {
82
+ type: "assistant_message",
83
+ message: { role: "assistant", content: full },
84
+ };
74
85
  return;
75
86
  }
76
87
  const token = j.message?.content;
77
- if (typeof token === 'string' && token) {
88
+ if (typeof token === "string" && token) {
78
89
  full += token;
79
- yield { type: 'token', token };
90
+ yield { type: "token", token };
80
91
  }
81
92
  }
82
93
  catch (e) {
83
- console.error('[DEBUG_LLM] stream_parse_error', { error: e });
94
+ console.error("[DEBUG_LLM] stream_parse_error", { error: e });
84
95
  }
85
96
  }
86
97
  }
@@ -90,14 +101,17 @@ export function ollamaAdapter(opts) {
90
101
  return (async () => {
91
102
  const toolsParam = (genOpts?.tools ?? []).map(toOllamaTool);
92
103
  const mapped = await mapMessagesWithImages();
93
- const baseBody = { model: opts.model, messages: mapped };
104
+ const baseBody = {
105
+ model: opts.model,
106
+ messages: mapped,
107
+ };
94
108
  if (toolsParam.length)
95
109
  baseBody.tools = toolsParam;
96
110
  const res = await fetch(`${baseUrl}/api/chat`, {
97
- method: 'POST',
111
+ method: "POST",
98
112
  headers: {
99
- 'Content-Type': 'application/json',
100
- Accept: 'application/json',
113
+ "Content-Type": "application/json",
114
+ Accept: "application/json",
101
115
  ...(opts.headers ?? {}),
102
116
  },
103
117
  body: JSON.stringify({ ...baseBody, stream: false }),
@@ -110,31 +124,40 @@ export function ollamaAdapter(opts) {
110
124
  details = j.error ?? j.message ?? raw;
111
125
  }
112
126
  catch (e) {
113
- console.error('[DEBUG_LLM] request_error', { error: e });
127
+ console.error("[DEBUG_LLM] request_error", { error: e });
114
128
  }
115
129
  throw new Error(`Ollama API error: ${res.status} ${res.statusText} — ${String(details).slice(0, 500)}`);
116
130
  }
117
131
  const data = raw ? JSON.parse(raw) : {};
118
- const choice = data?.message ?? {};
119
- const content = choice?.content;
120
- const tcs = Array.isArray(choice?.tool_calls)
121
- ? choice.tool_calls.map((tc) => ({ id: tc.id, name: tc.function?.name, arguments: safeJson(tc.function?.arguments) }))
132
+ const choice = data
133
+ .message ?? {};
134
+ const content = choice.content;
135
+ const tcs = Array.isArray(choice.tool_calls)
136
+ ? choice.tool_calls
137
+ .map((tc) => ({
138
+ id: tc.id ?? "",
139
+ name: tc.function?.name ?? "",
140
+ arguments: safeJson(tc.function?.arguments),
141
+ }))
142
+ .filter((tc) => tc.id && tc.name)
122
143
  : undefined;
123
- const out = { role: 'assistant', content: content ?? '' };
124
- if (tcs)
125
- out.tool_calls = tcs;
144
+ const out = {
145
+ role: "assistant",
146
+ content: typeof content === "string" ? content : "",
147
+ ...(tcs ? { tool_calls: tcs } : {}),
148
+ };
126
149
  return { message: out };
127
150
  })();
128
- }
151
+ });
129
152
  return {
130
153
  name: modelName,
131
154
  capabilities: { functionCall: true, streaming: true },
132
- generate: generate,
155
+ generate,
133
156
  };
134
157
  }
135
158
  function toOllamaTool(tool) {
136
159
  return {
137
- type: 'function',
160
+ type: "function",
138
161
  function: {
139
162
  name: tool.name,
140
163
  description: tool.description,
@@ -144,34 +167,43 @@ function toOllamaTool(tool) {
144
167
  }
145
168
  function toJsonSchema(schema) {
146
169
  if (!schema)
147
- return { type: 'object' };
170
+ return { type: "object" };
148
171
  const t = schema?._def?.typeName;
149
- if (t === 'ZodString')
150
- return { type: 'string' };
151
- if (t === 'ZodNumber')
152
- return { type: 'number' };
153
- if (t === 'ZodBoolean')
154
- return { type: 'boolean' };
155
- if (t === 'ZodArray')
156
- return { type: 'array', items: toJsonSchema(schema._def?.type) };
157
- if (t === 'ZodOptional')
158
- return toJsonSchema(schema._def?.innerType);
159
- if (t === 'ZodObject') {
160
- const shape = typeof schema._def?.shape === 'function' ? schema._def.shape() : schema._def?.shape;
172
+ if (t === "ZodString")
173
+ return { type: "string" };
174
+ if (t === "ZodNumber")
175
+ return { type: "number" };
176
+ if (t === "ZodBoolean")
177
+ return { type: "boolean" };
178
+ if (t === "ZodArray")
179
+ return {
180
+ type: "array",
181
+ items: toJsonSchema(schema?._def?.type),
182
+ };
183
+ if (t === "ZodOptional")
184
+ return toJsonSchema(schema?._def?.innerType);
185
+ if (t === "ZodObject") {
186
+ const shape = typeof schema?._def?.shape === "function"
187
+ ? schema._def?.shape?.()
188
+ : schema?._def?.shape;
161
189
  const props = {};
162
190
  const required = [];
163
191
  for (const [key, val] of Object.entries(shape ?? {})) {
164
192
  props[key] = toJsonSchema(val);
165
193
  const innerTypeName = val?._def?.typeName;
166
- if (innerTypeName !== 'ZodOptional' && innerTypeName !== 'ZodDefault')
194
+ if (innerTypeName !== "ZodOptional" && innerTypeName !== "ZodDefault")
167
195
  required.push(key);
168
196
  }
169
- return { type: 'object', properties: props, ...(required.length ? { required } : {}) };
197
+ return {
198
+ type: "object",
199
+ properties: props,
200
+ ...(required.length ? { required } : {}),
201
+ };
170
202
  }
171
- return { type: 'object' };
203
+ return { type: "object" };
172
204
  }
173
205
  function safeJson(s) {
174
- if (typeof s !== 'string')
206
+ if (typeof s !== "string")
175
207
  return s;
176
208
  try {
177
209
  return JSON.parse(s);
@@ -183,49 +215,56 @@ function safeJson(s) {
183
215
  // Accept OpenAI-style content parts or convenience fields and map to
184
216
  // Ollama's expected shape: { content: string, images?: string[] }
185
217
  function buildTextAndImages(m) {
186
- if (!m || typeof m !== 'object')
218
+ if (!m || typeof m !== "object")
187
219
  return {};
188
220
  const obj = m;
189
221
  // If content is parts, normalize
190
222
  if (Array.isArray(obj.content) || Array.isArray(obj.contentParts)) {
191
- const parts = Array.isArray(obj.content) ? obj.content : obj.contentParts;
223
+ const parts = Array.isArray(obj.content)
224
+ ? obj.content
225
+ : obj.contentParts;
192
226
  const texts = [];
193
227
  const images = [];
194
228
  for (const p of parts) {
195
- if (typeof p === 'string') {
229
+ if (typeof p === "string") {
196
230
  texts.push(p);
197
231
  continue;
198
232
  }
199
- if (!p || typeof p !== 'object')
233
+ if (!p || typeof p !== "object")
200
234
  continue;
201
235
  const po = p;
202
236
  const t = po.type;
203
- if (t === 'text' && typeof po.text === 'string') {
237
+ if (t === "text" && typeof po.text === "string") {
204
238
  texts.push(po.text);
205
239
  continue;
206
240
  }
207
- if (t === 'image_url') {
241
+ if (t === "image_url") {
208
242
  const iu = po.image_url;
209
- if (typeof iu === 'string')
243
+ if (typeof iu === "string")
210
244
  images.push(iu);
211
- else if (iu && typeof iu === 'object' && typeof iu.url === 'string')
245
+ else if (iu &&
246
+ typeof iu === "object" &&
247
+ typeof iu.url === "string")
212
248
  images.push(String(iu.url));
213
249
  continue;
214
250
  }
215
- if (t === 'image' && typeof po.url === 'string') {
251
+ if (t === "image" && typeof po.url === "string") {
216
252
  images.push(String(po.url));
217
253
  continue;
218
254
  }
219
- if (typeof po.image_url === 'string') {
255
+ if (typeof po.image_url === "string") {
220
256
  images.push(String(po.image_url));
221
257
  continue;
222
258
  }
223
- if (typeof po.image === 'string') {
259
+ if (typeof po.image === "string") {
224
260
  images.push(String(po.image));
225
261
  continue;
226
262
  }
227
263
  }
228
- return { content: texts.join('\n\n'), images: images.length ? images : undefined };
264
+ return {
265
+ content: texts.join("\n\n"),
266
+ images: images.length ? images : undefined,
267
+ };
229
268
  }
230
269
  // Otherwise, use content string (if any) and collect convenience images
231
270
  const images = [];
@@ -233,11 +272,11 @@ function buildTextAndImages(m) {
233
272
  images.push(...obj.images);
234
273
  if (Array.isArray(obj.image_urls))
235
274
  images.push(...obj.image_urls);
236
- if (typeof obj.image_url === 'string')
275
+ if (typeof obj.image_url === "string")
237
276
  images.push(obj.image_url);
238
- if (typeof obj.image === 'string')
277
+ if (typeof obj.image === "string")
239
278
  images.push(obj.image);
240
- const content = typeof obj.content === 'string' ? obj.content : undefined;
279
+ const content = typeof obj.content === "string" ? obj.content : undefined;
241
280
  return { content, images: images.length ? images : undefined };
242
281
  }
243
282
  async function toBase64Images(images) {
@@ -253,11 +292,11 @@ function isDataUrl(s) {
253
292
  return /^data:/i.test(s);
254
293
  }
255
294
  function fromDataUrl(s) {
256
- const i = s.indexOf(',');
257
- return i >= 0 ? s.slice(i + 1) : '';
295
+ const i = s.indexOf(",");
296
+ return i >= 0 ? s.slice(i + 1) : "";
258
297
  }
259
298
  function isProbablyBase64(s) {
260
- if (!s || /[:\/]/.test(s))
299
+ if (!s || /[:/]/.test(s))
261
300
  return false; // exclude URLs
262
301
  // Basic base64 check: valid chars and length % 4 == 0
263
302
  if (s.length % 4 !== 0)
@@ -272,7 +311,7 @@ async function toBase64(src) {
272
311
  if (!res.ok)
273
312
  throw new Error(`Failed to fetch image: ${res.status} ${res.statusText}`);
274
313
  const buf = Buffer.from(await res.arrayBuffer());
275
- return buf.toString('base64');
314
+ return buf.toString("base64");
276
315
  }
277
316
  return isProbablyBase64(src) ? src : src;
278
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisu-ai/adapter-ollama",
3
- "version": "9.0.1",
3
+ "version": "9.0.2",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -21,7 +21,7 @@
21
21
  "url": "https://github.com/finger-gun/sisu/issues"
22
22
  },
23
23
  "peerDependencies": {
24
- "@sisu-ai/core": "^2.3.1"
24
+ "@sisu-ai/core": "^2.3.2"
25
25
  },
26
26
  "keywords": [
27
27
  "sisu",