@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 +26 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +112 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +112 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +19 -0
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.
|
package/dist/index.d.ts
ADDED
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,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
|
+
}
|