@sisu-ai/adapter-ollama 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @sisu-ai/adapter-ollama
2
+
3
+ Ollama Chat adapter with native tools support.
4
+
5
+ ## Setup
6
+ - Start Ollama locally: `ollama serve`
7
+ - Pull a tools-capable model: `ollama pull llama3.1:latest`
8
+
9
+ ## Usage
10
+ ```ts
11
+ import { ollamaAdapter } from '@sisu-ai/adapter-ollama';
12
+
13
+ const model = ollamaAdapter({ model: 'llama3.1' });
14
+ // or with custom base URL: { baseUrl: 'http://localhost:11435' }
15
+
16
+ // Works with @sisu-ai/mw-tool-calling — tools are passed via GenerateOptions.tools
17
+ ```
18
+
19
+ ## Tools
20
+ - Accepts `GenerateOptions.tools` and sends them to Ollama under `tools`.
21
+ - Parses `message.tool_calls` into `{ id, name, arguments }` for the tool loop.
22
+ - Sends assistant `tool_calls` and `tool` messages back to Ollama for follow-up.
23
+
24
+ ## Notes
25
+ - Tool choice forcing is model-dependent; current loop asks for tools on first turn and plain completion on second.
26
+ - Streaming can be added via Ollama's streaming API if desired.
@@ -0,0 +1,7 @@
1
+ import type { LLM } from '@sisu-ai/core';
2
+ export interface OllamaAdapterOptions {
3
+ model: string;
4
+ baseUrl?: string;
5
+ headers?: Record<string, string>;
6
+ }
7
+ export declare function ollamaAdapter(opts: OllamaAdapterOptions): LLM;
package/dist/index.js ADDED
@@ -0,0 +1,112 @@
1
+ export function ollamaAdapter(opts) {
2
+ const envBase = process.env.OLLAMA_BASE_URL || process.env.BASE_URL;
3
+ const baseUrl = (opts.baseUrl ?? envBase ?? 'http://localhost:11434').replace(/\/$/, '');
4
+ const modelName = `ollama:${opts.model}`;
5
+ return {
6
+ name: modelName,
7
+ capabilities: { functionCall: true, streaming: false },
8
+ async generate(messages, genOpts) {
9
+ // Map messages to Ollama format; include assistant tool_calls and tool messages
10
+ const mapped = messages.map((m) => {
11
+ const base = { role: m.role };
12
+ if (m.role === 'assistant' && Array.isArray(m.tool_calls)) {
13
+ base.tool_calls = m.tool_calls.map((tc) => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: (tc.arguments ?? {}) } }));
14
+ base.content = m.content ? String(m.content) : null;
15
+ }
16
+ else if (m.role === 'tool') {
17
+ base.content = String(m.content ?? '');
18
+ if (m.tool_call_id)
19
+ base.tool_call_id = m.tool_call_id;
20
+ if (m.name && !m.tool_call_id)
21
+ base.name = m.name;
22
+ }
23
+ else {
24
+ base.content = String(m.content ?? '');
25
+ }
26
+ return base;
27
+ });
28
+ const toolsParam = (genOpts?.tools ?? []).map(toOllamaTool);
29
+ const body = { model: opts.model, messages: mapped, stream: false };
30
+ if (toolsParam.length)
31
+ body.tools = toolsParam;
32
+ const res = await fetch(`${baseUrl}/api/chat`, {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ Accept: 'application/json',
37
+ ...(opts.headers ?? {}),
38
+ },
39
+ body: JSON.stringify(body),
40
+ });
41
+ const raw = await res.text();
42
+ if (!res.ok) {
43
+ let details = raw;
44
+ try {
45
+ const j = JSON.parse(raw);
46
+ details = j.error ?? j.message ?? raw;
47
+ }
48
+ catch { }
49
+ throw new Error(`Ollama API error: ${res.status} ${res.statusText} — ${String(details).slice(0, 500)}`);
50
+ }
51
+ const data = raw ? JSON.parse(raw) : {};
52
+ // /api/chat response example (non-stream): { message: { role:'assistant', content:'...', tool_calls?: [...] }, done: true }
53
+ const choice = data?.message ?? {};
54
+ const content = choice?.content ?? '';
55
+ const tcs = Array.isArray(choice?.tool_calls)
56
+ ? choice.tool_calls.map((tc) => ({ id: tc.id, name: tc.function?.name, arguments: safeJson(tc.function?.arguments) }))
57
+ : undefined;
58
+ const out = { role: 'assistant', content: String(content ?? '') };
59
+ if (tcs)
60
+ out.tool_calls = tcs;
61
+ return { message: out };
62
+ },
63
+ };
64
+ }
65
+ function toOllamaTool(tool) {
66
+ return {
67
+ type: 'function',
68
+ function: {
69
+ name: tool.name,
70
+ description: tool.description,
71
+ parameters: toJsonSchema(tool.schema),
72
+ },
73
+ };
74
+ }
75
+ function toJsonSchema(schema) {
76
+ if (!schema)
77
+ return { type: 'object' };
78
+ const t = schema?._def?.typeName;
79
+ if (t === 'ZodString')
80
+ return { type: 'string' };
81
+ if (t === 'ZodNumber')
82
+ return { type: 'number' };
83
+ if (t === 'ZodBoolean')
84
+ return { type: 'boolean' };
85
+ if (t === 'ZodArray')
86
+ return { type: 'array', items: toJsonSchema(schema._def?.type) };
87
+ if (t === 'ZodOptional')
88
+ return toJsonSchema(schema._def?.innerType);
89
+ if (t === 'ZodObject') {
90
+ const shape = typeof schema._def?.shape === 'function' ? schema._def.shape() : schema._def?.shape;
91
+ const props = {};
92
+ const required = [];
93
+ for (const [key, val] of Object.entries(shape ?? {})) {
94
+ props[key] = toJsonSchema(val);
95
+ const innerTypeName = val?._def?.typeName;
96
+ if (innerTypeName !== 'ZodOptional' && innerTypeName !== 'ZodDefault')
97
+ required.push(key);
98
+ }
99
+ return { type: 'object', properties: props, ...(required.length ? { required } : {}) };
100
+ }
101
+ return { type: 'object' };
102
+ }
103
+ function safeJson(s) {
104
+ if (typeof s !== 'string')
105
+ return s;
106
+ try {
107
+ return JSON.parse(s);
108
+ }
109
+ catch {
110
+ return s;
111
+ }
112
+ }
@@ -0,0 +1,7 @@
1
+ import type { LLM } from '@sisu-ai/core';
2
+ export interface OllamaAdapterOptions {
3
+ model: string;
4
+ baseUrl?: string;
5
+ headers?: Record<string, string>;
6
+ }
7
+ export declare function ollamaAdapter(opts: OllamaAdapterOptions): LLM;
@@ -0,0 +1,112 @@
1
+ export function ollamaAdapter(opts) {
2
+ const envBase = process.env.OLLAMA_BASE_URL || process.env.BASE_URL;
3
+ const baseUrl = (opts.baseUrl ?? envBase ?? 'http://localhost:11434').replace(/\/$/, '');
4
+ const modelName = `ollama:${opts.model}`;
5
+ return {
6
+ name: modelName,
7
+ capabilities: { functionCall: true, streaming: false },
8
+ async generate(messages, genOpts) {
9
+ // Map messages to Ollama format; include assistant tool_calls and tool messages
10
+ const mapped = messages.map((m) => {
11
+ const base = { role: m.role };
12
+ if (m.role === 'assistant' && Array.isArray(m.tool_calls)) {
13
+ base.tool_calls = m.tool_calls.map((tc) => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: (tc.arguments ?? {}) } }));
14
+ base.content = m.content ? String(m.content) : null;
15
+ }
16
+ else if (m.role === 'tool') {
17
+ base.content = String(m.content ?? '');
18
+ if (m.tool_call_id)
19
+ base.tool_call_id = m.tool_call_id;
20
+ if (m.name && !m.tool_call_id)
21
+ base.name = m.name;
22
+ }
23
+ else {
24
+ base.content = String(m.content ?? '');
25
+ }
26
+ return base;
27
+ });
28
+ const toolsParam = (genOpts?.tools ?? []).map(toOllamaTool);
29
+ const body = { model: opts.model, messages: mapped, stream: false };
30
+ if (toolsParam.length)
31
+ body.tools = toolsParam;
32
+ const res = await fetch(`${baseUrl}/api/chat`, {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ Accept: 'application/json',
37
+ ...(opts.headers ?? {}),
38
+ },
39
+ body: JSON.stringify(body),
40
+ });
41
+ const raw = await res.text();
42
+ if (!res.ok) {
43
+ let details = raw;
44
+ try {
45
+ const j = JSON.parse(raw);
46
+ details = j.error ?? j.message ?? raw;
47
+ }
48
+ catch { }
49
+ throw new Error(`Ollama API error: ${res.status} ${res.statusText} — ${String(details).slice(0, 500)}`);
50
+ }
51
+ const data = raw ? JSON.parse(raw) : {};
52
+ // /api/chat response example (non-stream): { message: { role:'assistant', content:'...', tool_calls?: [...] }, done: true }
53
+ const choice = data?.message ?? {};
54
+ const content = choice?.content ?? '';
55
+ const tcs = Array.isArray(choice?.tool_calls)
56
+ ? choice.tool_calls.map((tc) => ({ id: tc.id, name: tc.function?.name, arguments: safeJson(tc.function?.arguments) }))
57
+ : undefined;
58
+ const out = { role: 'assistant', content: String(content ?? '') };
59
+ if (tcs)
60
+ out.tool_calls = tcs;
61
+ return { message: out };
62
+ },
63
+ };
64
+ }
65
+ function toOllamaTool(tool) {
66
+ return {
67
+ type: 'function',
68
+ function: {
69
+ name: tool.name,
70
+ description: tool.description,
71
+ parameters: toJsonSchema(tool.schema),
72
+ },
73
+ };
74
+ }
75
+ function toJsonSchema(schema) {
76
+ if (!schema)
77
+ return { type: 'object' };
78
+ const t = schema?._def?.typeName;
79
+ if (t === 'ZodString')
80
+ return { type: 'string' };
81
+ if (t === 'ZodNumber')
82
+ return { type: 'number' };
83
+ if (t === 'ZodBoolean')
84
+ return { type: 'boolean' };
85
+ if (t === 'ZodArray')
86
+ return { type: 'array', items: toJsonSchema(schema._def?.type) };
87
+ if (t === 'ZodOptional')
88
+ return toJsonSchema(schema._def?.innerType);
89
+ if (t === 'ZodObject') {
90
+ const shape = typeof schema._def?.shape === 'function' ? schema._def.shape() : schema._def?.shape;
91
+ const props = {};
92
+ const required = [];
93
+ for (const [key, val] of Object.entries(shape ?? {})) {
94
+ props[key] = toJsonSchema(val);
95
+ const innerTypeName = val?._def?.typeName;
96
+ if (innerTypeName !== 'ZodOptional' && innerTypeName !== 'ZodDefault')
97
+ required.push(key);
98
+ }
99
+ return { type: 'object', properties: props, ...(required.length ? { required } : {}) };
100
+ }
101
+ return { type: 'object' };
102
+ }
103
+ function safeJson(s) {
104
+ if (typeof s !== 'string')
105
+ return s;
106
+ try {
107
+ return JSON.parse(s);
108
+ }
109
+ catch {
110
+ return s;
111
+ }
112
+ }
@@ -0,0 +1 @@
1
+ {"root":["../src/index.ts"],"version":"5.9.2"}
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@sisu-ai/adapter-ollama",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc -b"
15
+ },
16
+ "peerDependencies": {
17
+ "@sisu-ai/core": "0.2.0"
18
+ }
19
+ }