@pi-in-go/pigpen-ollama-native 0.1.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/CREDITS.md +24 -0
- package/LICENSE +22 -0
- package/README.md +81 -0
- package/extensions/ollama-native/chat.go +241 -0
- package/extensions/ollama-native/chat_test.go +287 -0
- package/extensions/ollama-native/extension.go +170 -0
- package/extensions/ollama-native/go.mod +6 -0
- package/extensions/ollama-native/go.sum +2 -0
- package/extensions/ollama-native/helpers_test.go +144 -0
- package/extensions/ollama-native/models.go +235 -0
- package/extensions/ollama-native/models_test.go +223 -0
- package/extensions/ollama-native/request.go +184 -0
- package/extensions/ollama-native/testdata/chat_404.json +1 -0
- package/extensions/ollama-native/testdata/chat_length.ndjson +2 -0
- package/extensions/ollama-native/testdata/chat_midstream_error.ndjson +2 -0
- package/extensions/ollama-native/testdata/chat_text.ndjson +4 -0
- package/extensions/ollama-native/testdata/chat_thinking.ndjson +4 -0
- package/extensions/ollama-native/testdata/chat_toolcalls.ndjson +4 -0
- package/extensions/ollama-native/testdata/chat_truncated.ndjson +1 -0
- package/extensions/ollama-native/testdata/show_granite4.1_3b.json +1 -0
- package/extensions/ollama-native/testdata/show_nomic-embed-text_latest.json +1 -0
- package/extensions/ollama-native/testdata/show_qwen3_8b.json +1 -0
- package/extensions/ollama-native/testdata/tags.json +1 -0
- package/extensions/ollama-native/testdata/tags_empty.json +1 -0
- package/package.json +39 -0
- package/provenance.json +7 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
package ollamanative
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
sdk "github.com/MichaelKinsy/PiG/extensions/sdk"
|
|
7
|
+
"net/http"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func TestNormalizeHost(t *testing.T) {
|
|
13
|
+
for in, want := range map[string]string{
|
|
14
|
+
"": "http://127.0.0.1:11434",
|
|
15
|
+
" ": "http://127.0.0.1:11434",
|
|
16
|
+
"http://127.0.0.1:11434": "http://127.0.0.1:11434",
|
|
17
|
+
"http://box:8080/": "http://box:8080",
|
|
18
|
+
"box": "http://box:11434",
|
|
19
|
+
"box:8080": "http://box:8080",
|
|
20
|
+
":9000": "http://127.0.0.1:9000",
|
|
21
|
+
"https://ollama.example.com": "https://ollama.example.com:443",
|
|
22
|
+
"https://o.example.com:8443": "https://o.example.com:8443",
|
|
23
|
+
"http://box": "http://box:80",
|
|
24
|
+
"[::1]:11434": "http://[::1]:11434",
|
|
25
|
+
} {
|
|
26
|
+
if got := normalizeHost(in); got != want {
|
|
27
|
+
t.Errorf("normalizeHost(%q) = %q, want %q", in, got, want)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Registering the provider must not touch the network: pig starts without Ollama
|
|
33
|
+
// running, and a model list is only fetched when the host asks for a refresh.
|
|
34
|
+
func TestConstructionAndGetModelsMakeNoRequests(t *testing.T) {
|
|
35
|
+
f := newFake(t)
|
|
36
|
+
t.Setenv("OLLAMA_HOST", f.URL)
|
|
37
|
+
_ = Extension()
|
|
38
|
+
p := newProvider(newClient(f.URL))
|
|
39
|
+
models, err := p.GetModels()
|
|
40
|
+
if err != nil || len(models) != 0 {
|
|
41
|
+
t.Fatalf("GetModels before any refresh = %v, %v; want empty", models, err)
|
|
42
|
+
}
|
|
43
|
+
if f.count() != 0 {
|
|
44
|
+
t.Fatalf("requests at startup: %v", f.requests)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func TestDiscoverReadsModelsFromTagsAndShow(t *testing.T) {
|
|
49
|
+
f := newFake(t)
|
|
50
|
+
models, err := newClient(f.URL).discover(context.Background())
|
|
51
|
+
if err != nil {
|
|
52
|
+
t.Fatal(err)
|
|
53
|
+
}
|
|
54
|
+
// nomic-embed-text only embeds: it cannot chat, so it is not offered.
|
|
55
|
+
if len(models) != 2 {
|
|
56
|
+
t.Fatalf("models = %v", models)
|
|
57
|
+
}
|
|
58
|
+
g, q := models[0], models[1]
|
|
59
|
+
if g["id"] != "granite4.1:3b" || g["api"] != "ollama-native" || g["contextWindow"] != 131072 || g["reasoning"] != false {
|
|
60
|
+
t.Errorf("granite = %v", g)
|
|
61
|
+
}
|
|
62
|
+
if in := g["input"].([]string); len(in) != 1 || in[0] != "text" {
|
|
63
|
+
t.Errorf("granite input = %v", in)
|
|
64
|
+
}
|
|
65
|
+
if q["id"] != "qwen3:8b" || q["contextWindow"] != 40960 || q["reasoning"] != true {
|
|
66
|
+
t.Errorf("qwen3 = %v", q)
|
|
67
|
+
}
|
|
68
|
+
if in := q["input"].([]string); len(in) != 2 || in[1] != "image" {
|
|
69
|
+
t.Errorf("qwen3 input = %v", in)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// /api/show is best effort: a model it cannot describe still gets listed, with
|
|
74
|
+
// plain defaults, so one bad model does not hide the rest.
|
|
75
|
+
func TestDiscoverFallsBackToDefaultsWhenShowFails(t *testing.T) {
|
|
76
|
+
f := newFake(t)
|
|
77
|
+
f.tags = "tags_empty.json"
|
|
78
|
+
f.Server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
79
|
+
if r.URL.Path == "/api/tags" {
|
|
80
|
+
w.Write([]byte(`{"models":[{"name":"mystery:1b"}]}`))
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
http.Error(w, `{"error":"boom"}`, 500)
|
|
84
|
+
})
|
|
85
|
+
models, err := newClient(f.URL).discover(context.Background())
|
|
86
|
+
if err != nil || len(models) != 1 {
|
|
87
|
+
t.Fatalf("models = %v, err = %v", models, err)
|
|
88
|
+
}
|
|
89
|
+
m := models[0]
|
|
90
|
+
if m["id"] != "mystery:1b" || m["contextWindow"] != defaultContextWindow || m["reasoning"] != false {
|
|
91
|
+
t.Errorf("defaults = %v", m)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func TestDiscoverEmptyListIsNotAnError(t *testing.T) {
|
|
96
|
+
f := newFake(t)
|
|
97
|
+
f.tags = "tags_empty.json"
|
|
98
|
+
models, err := newClient(f.URL).discover(context.Background())
|
|
99
|
+
if err != nil || len(models) != 0 {
|
|
100
|
+
t.Fatalf("models = %v, err = %v", models, err)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func TestDiscoverReportsOllamaNotRunning(t *testing.T) {
|
|
105
|
+
f := newFake(t)
|
|
106
|
+
url := f.URL
|
|
107
|
+
f.Close()
|
|
108
|
+
_, err := newClient(url).discover(context.Background())
|
|
109
|
+
if err == nil || !strings.Contains(err.Error(), "not reachable at "+url) || !strings.Contains(err.Error(), "OLLAMA_HOST") {
|
|
110
|
+
t.Fatalf("err = %v", err)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func TestRefreshModelsPublishesAndSwapsTheCatalog(t *testing.T) {
|
|
115
|
+
f := newFake(t)
|
|
116
|
+
p := newProvider(newClient(f.URL))
|
|
117
|
+
|
|
118
|
+
// Offline: no request, catalog untouched.
|
|
119
|
+
if err := p.RefreshModels(sdk.RefreshModelsContext{AllowNetwork: false, Signal: context.Background(), Publish: func(sdk.ModelsPublication) (bool, error) { t.Fatal("published offline"); return false, nil }}); err != nil {
|
|
120
|
+
t.Fatal(err)
|
|
121
|
+
}
|
|
122
|
+
if f.count() != 0 {
|
|
123
|
+
t.Fatalf("offline refresh made requests: %v", f.requests)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
published := 0
|
|
127
|
+
err := p.RefreshModels(sdk.RefreshModelsContext{AllowNetwork: true, Signal: context.Background(), Publish: func(pub sdk.ModelsPublication) (bool, error) {
|
|
128
|
+
published++
|
|
129
|
+
// The catalog is stored for the next start, which must not need Ollama.
|
|
130
|
+
var entry struct {
|
|
131
|
+
Models []map[string]any `json:"models"`
|
|
132
|
+
CheckedAt float64 `json:"checkedAt"`
|
|
133
|
+
}
|
|
134
|
+
if err := json.Unmarshal(pub.Persist, &entry); err != nil || len(entry.Models) != 2 || entry.CheckedAt == 0 {
|
|
135
|
+
t.Errorf("persist = %s (%v)", pub.Persist, err)
|
|
136
|
+
}
|
|
137
|
+
if before, _ := p.GetModels(); len(before) != 0 {
|
|
138
|
+
t.Error("catalog changed before the host applied the update")
|
|
139
|
+
}
|
|
140
|
+
return true, pub.Update()
|
|
141
|
+
}})
|
|
142
|
+
if err != nil || published != 1 {
|
|
143
|
+
t.Fatalf("refresh err = %v, published = %d", err, published)
|
|
144
|
+
}
|
|
145
|
+
models, _ := p.GetModels()
|
|
146
|
+
if len(models) != 2 || models[0]["id"] != "granite4.1:3b" {
|
|
147
|
+
t.Fatalf("models after refresh = %v", models)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// A failed refresh keeps the last good catalog and reports the reason.
|
|
152
|
+
func TestRefreshModelsKeepsLastCatalogWhenOllamaStops(t *testing.T) {
|
|
153
|
+
f := newFake(t)
|
|
154
|
+
c := newClient(f.URL)
|
|
155
|
+
p := newProvider(c)
|
|
156
|
+
publish := func(pub sdk.ModelsPublication) (bool, error) { return true, pub.Update() }
|
|
157
|
+
if err := p.RefreshModels(sdk.RefreshModelsContext{AllowNetwork: true, Signal: context.Background(), Publish: publish}); err != nil {
|
|
158
|
+
t.Fatal(err)
|
|
159
|
+
}
|
|
160
|
+
f.Close()
|
|
161
|
+
err := p.RefreshModels(sdk.RefreshModelsContext{AllowNetwork: true, Signal: context.Background(), Publish: publish})
|
|
162
|
+
if err == nil || !strings.Contains(err.Error(), "not reachable") {
|
|
163
|
+
t.Fatalf("err = %v", err)
|
|
164
|
+
}
|
|
165
|
+
if models, _ := p.GetModels(); len(models) != 2 {
|
|
166
|
+
t.Fatalf("catalog dropped: %v", models)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// PiG calls RefreshModels once without the network at start, handing back what
|
|
171
|
+
// was stored. That is how `pig -p --model ollama-native/x` finds its model with
|
|
172
|
+
// Ollama not even asked.
|
|
173
|
+
func TestOfflinePassRestoresTheStoredCatalogWithoutRequests(t *testing.T) {
|
|
174
|
+
f := newFake(t)
|
|
175
|
+
// A first process refreshes and persists.
|
|
176
|
+
var stored json.RawMessage
|
|
177
|
+
first := newProvider(newClient(f.URL))
|
|
178
|
+
err := first.RefreshModels(sdk.RefreshModelsContext{AllowNetwork: true, Signal: context.Background(), Publish: func(pub sdk.ModelsPublication) (bool, error) {
|
|
179
|
+
stored = pub.Persist
|
|
180
|
+
return true, pub.Update()
|
|
181
|
+
}})
|
|
182
|
+
if err != nil {
|
|
183
|
+
t.Fatal(err)
|
|
184
|
+
}
|
|
185
|
+
before := f.count()
|
|
186
|
+
|
|
187
|
+
// A second process starts: PiG hands the stored entry back, as decoded JSON.
|
|
188
|
+
var entry map[string]any
|
|
189
|
+
if err := json.Unmarshal(stored, &entry); err != nil {
|
|
190
|
+
t.Fatal(err)
|
|
191
|
+
}
|
|
192
|
+
second := newProvider(newClient(f.URL))
|
|
193
|
+
applied := 0
|
|
194
|
+
err = second.RefreshModels(sdk.RefreshModelsContext{AllowNetwork: false, Stored: entry, Signal: context.Background(), Publish: func(pub sdk.ModelsPublication) (bool, error) {
|
|
195
|
+
applied++
|
|
196
|
+
if pub.Persist != nil {
|
|
197
|
+
t.Error("the offline pass must not rewrite the store")
|
|
198
|
+
}
|
|
199
|
+
return true, pub.Update()
|
|
200
|
+
}})
|
|
201
|
+
if err != nil || applied != 1 {
|
|
202
|
+
t.Fatalf("err = %v, applied = %d", err, applied)
|
|
203
|
+
}
|
|
204
|
+
if f.count() != before {
|
|
205
|
+
t.Fatalf("the offline pass made requests: %v", f.requests[before:])
|
|
206
|
+
}
|
|
207
|
+
models, _ := second.GetModels()
|
|
208
|
+
if len(models) != 2 || models[1]["id"] != "qwen3:8b" || models[1]["api"] != "ollama-native" {
|
|
209
|
+
t.Fatalf("restored = %v", models)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
func TestOfflinePassIgnoresAMalformedStore(t *testing.T) {
|
|
214
|
+
p := newProvider(newClient("http://127.0.0.1:1"))
|
|
215
|
+
err := p.RefreshModels(sdk.RefreshModelsContext{AllowNetwork: false, Stored: map[string]any{"models": []any{"junk", map[string]any{"name": "no id"}, map[string]any{"id": "ok:1b", "api": "ollama-native"}}},
|
|
216
|
+
Publish: func(pub sdk.ModelsPublication) (bool, error) { return true, pub.Update() }})
|
|
217
|
+
if err != nil {
|
|
218
|
+
t.Fatal(err)
|
|
219
|
+
}
|
|
220
|
+
if models, _ := p.GetModels(); len(models) != 1 || models[0]["id"] != "ok:1b" {
|
|
221
|
+
t.Fatalf("models = %v", models)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
package ollamanative
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"slices"
|
|
5
|
+
"strings"
|
|
6
|
+
|
|
7
|
+
sdk "github.com/MichaelKinsy/PiG/extensions/sdk"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// chatRequest turns PiG's transcript into an /api/chat body.
|
|
11
|
+
//
|
|
12
|
+
// The transcript is {"messages": [...]}. System messages also carry the tool
|
|
13
|
+
// set: "toolsAdded" and "toolsRemoved" are changes, applied in order, so the
|
|
14
|
+
// tools on offer are what is left after the last message.
|
|
15
|
+
func chatRequest(model, transcript map[string]any, opts sdk.ProviderStreamOptions) map[string]any {
|
|
16
|
+
req := map[string]any{"model": model["id"], "stream": true}
|
|
17
|
+
var messages []map[string]any
|
|
18
|
+
var instructions []string
|
|
19
|
+
sections := map[string]string{}
|
|
20
|
+
var toolNames []string
|
|
21
|
+
tools := map[string]map[string]any{}
|
|
22
|
+
|
|
23
|
+
raw, _ := transcript["messages"].([]any)
|
|
24
|
+
for _, item := range raw {
|
|
25
|
+
m, _ := item.(map[string]any)
|
|
26
|
+
switch m["role"] {
|
|
27
|
+
case "system":
|
|
28
|
+
for _, t := range list(m["toolsRemoved"]) {
|
|
29
|
+
name, _ := t["name"].(string)
|
|
30
|
+
delete(tools, name)
|
|
31
|
+
toolNames = remove(toolNames, name)
|
|
32
|
+
}
|
|
33
|
+
for _, t := range list(m["toolsAdded"]) {
|
|
34
|
+
name, _ := t["name"].(string)
|
|
35
|
+
if _, seen := tools[name]; !seen {
|
|
36
|
+
toolNames = append(toolNames, name)
|
|
37
|
+
}
|
|
38
|
+
tools[name] = t
|
|
39
|
+
}
|
|
40
|
+
if text, _ := textAndImages(m["content"]); text != "" {
|
|
41
|
+
instructions = append(instructions, text)
|
|
42
|
+
}
|
|
43
|
+
if named, ok := m["sections"].(map[string]any); ok {
|
|
44
|
+
for name, value := range named {
|
|
45
|
+
if text, ok := value.(string); ok {
|
|
46
|
+
sections[name] = text
|
|
47
|
+
} else {
|
|
48
|
+
delete(sections, name) // null removes the section
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
case "user":
|
|
53
|
+
text, images := textAndImages(m["content"])
|
|
54
|
+
msg := map[string]any{"role": "user", "content": text}
|
|
55
|
+
if len(images) > 0 {
|
|
56
|
+
msg["images"] = images
|
|
57
|
+
}
|
|
58
|
+
messages = append(messages, msg)
|
|
59
|
+
case "assistant":
|
|
60
|
+
// Thinking is not sent back: Ollama's models are not trained on it as input.
|
|
61
|
+
var text []string
|
|
62
|
+
var calls []map[string]any
|
|
63
|
+
for _, b := range list(m["content"]) {
|
|
64
|
+
switch b["type"] {
|
|
65
|
+
case "text":
|
|
66
|
+
if s, _ := b["text"].(string); s != "" {
|
|
67
|
+
text = append(text, s)
|
|
68
|
+
}
|
|
69
|
+
case "toolCall":
|
|
70
|
+
args, _ := b["arguments"].(map[string]any)
|
|
71
|
+
if args == nil {
|
|
72
|
+
args = map[string]any{}
|
|
73
|
+
}
|
|
74
|
+
calls = append(calls, map[string]any{"function": map[string]any{"name": b["name"], "arguments": args}})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if len(text) == 0 && len(calls) == 0 {
|
|
78
|
+
continue // an aborted or failed turn that said nothing
|
|
79
|
+
}
|
|
80
|
+
msg := map[string]any{"role": "assistant", "content": strings.Join(text, "\n")}
|
|
81
|
+
if len(calls) > 0 {
|
|
82
|
+
msg["tool_calls"] = calls
|
|
83
|
+
}
|
|
84
|
+
messages = append(messages, msg)
|
|
85
|
+
case "toolResult":
|
|
86
|
+
text, images := textAndImages(m["content"])
|
|
87
|
+
msg := map[string]any{"role": "tool", "tool_name": m["toolName"], "content": text}
|
|
88
|
+
if len(images) > 0 {
|
|
89
|
+
msg["images"] = images
|
|
90
|
+
}
|
|
91
|
+
messages = append(messages, msg)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
names := make([]string, 0, len(sections))
|
|
95
|
+
for name, text := range sections {
|
|
96
|
+
if text != "" {
|
|
97
|
+
names = append(names, name)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
slices.Sort(names)
|
|
101
|
+
for _, name := range names {
|
|
102
|
+
instructions = append(instructions, sections[name])
|
|
103
|
+
}
|
|
104
|
+
if len(instructions) > 0 {
|
|
105
|
+
messages = append([]map[string]any{{"role": "system", "content": strings.Join(instructions, "\n\n")}}, messages...)
|
|
106
|
+
}
|
|
107
|
+
req["messages"] = messages
|
|
108
|
+
|
|
109
|
+
if len(toolNames) > 0 {
|
|
110
|
+
offered := make([]map[string]any, 0, len(toolNames))
|
|
111
|
+
for _, name := range toolNames {
|
|
112
|
+
t := tools[name]
|
|
113
|
+
params, _ := t["parameters"].(map[string]any)
|
|
114
|
+
if params == nil {
|
|
115
|
+
params = map[string]any{"type": "object", "properties": map[string]any{}}
|
|
116
|
+
}
|
|
117
|
+
offered = append(offered, map[string]any{"type": "function", "function": map[string]any{
|
|
118
|
+
"name": name, "description": t["description"], "parameters": params}})
|
|
119
|
+
}
|
|
120
|
+
req["tools"] = offered
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
options := map[string]any{}
|
|
124
|
+
if v, ok := opts.Values["temperature"].(float64); ok {
|
|
125
|
+
options["temperature"] = v
|
|
126
|
+
}
|
|
127
|
+
if v, ok := opts.Values["maxTokens"].(float64); ok && v > 0 {
|
|
128
|
+
options["num_predict"] = int(v)
|
|
129
|
+
}
|
|
130
|
+
if len(options) > 0 {
|
|
131
|
+
req["options"] = options
|
|
132
|
+
}
|
|
133
|
+
// Ollama refuses `think` for a model without the capability, so it is sent
|
|
134
|
+
// only to models the catalog marked as reasoning, and only when asked for.
|
|
135
|
+
if reasoning, _ := model["reasoning"].(bool); reasoning {
|
|
136
|
+
if on, ok := opts.Values["thinkingEnabled"].(bool); ok {
|
|
137
|
+
req["think"] = on
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return req
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func list(v any) []map[string]any {
|
|
144
|
+
items, _ := v.([]any)
|
|
145
|
+
out := make([]map[string]any, 0, len(items))
|
|
146
|
+
for _, item := range items {
|
|
147
|
+
if m, ok := item.(map[string]any); ok {
|
|
148
|
+
out = append(out, m)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func remove(names []string, name string) []string {
|
|
155
|
+
out := names[:0]
|
|
156
|
+
for _, n := range names {
|
|
157
|
+
if n != name {
|
|
158
|
+
out = append(out, n)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return out
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// textAndImages reads message content, which is a string or a list of blocks.
|
|
165
|
+
// Text blocks are joined with newlines; image blocks give their base64 data.
|
|
166
|
+
func textAndImages(content any) (string, []string) {
|
|
167
|
+
if s, ok := content.(string); ok {
|
|
168
|
+
return s, nil
|
|
169
|
+
}
|
|
170
|
+
var text, images []string
|
|
171
|
+
for _, b := range list(content) {
|
|
172
|
+
switch b["type"] {
|
|
173
|
+
case "text":
|
|
174
|
+
if s, _ := b["text"].(string); s != "" {
|
|
175
|
+
text = append(text, s)
|
|
176
|
+
}
|
|
177
|
+
case "image":
|
|
178
|
+
if s, _ := b["data"].(string); s != "" {
|
|
179
|
+
images = append(images, s)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return strings.Join(text, "\n"), images
|
|
184
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"error":"model 'nope:1b' not found"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"model":"granite4.1:3b","created_at":"2026-10-05T20:46:01.001Z","message":{"role":"assistant","content":"Long answer"},"done":false}
|
|
2
|
+
{"model":"granite4.1:3b","created_at":"2026-10-05T20:46:01.031Z","message":{"role":"assistant","content":""},"done_reason":"length","done":true,"prompt_eval_count":8,"eval_count":2}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"model":"granite4.1:3b","created_at":"2026-10-05T20:44:01.001Z","message":{"role":"assistant","content":"Partial "},"done":false}
|
|
2
|
+
{"error":"model runner has unexpectedly stopped, this may be due to resource limitations or an internal error, check ollama server logs for details"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"model":"granite4.1:3b","created_at":"2026-10-05T20:41:07.101Z","message":{"role":"assistant","content":"Hel"},"done":false}
|
|
2
|
+
{"model":"granite4.1:3b","created_at":"2026-10-05T20:41:07.131Z","message":{"role":"assistant","content":"lo"},"done":false}
|
|
3
|
+
{"model":"granite4.1:3b","created_at":"2026-10-05T20:41:07.161Z","message":{"role":"assistant","content":", world."},"done":false}
|
|
4
|
+
{"model":"granite4.1:3b","created_at":"2026-10-05T20:41:07.190Z","message":{"role":"assistant","content":""},"done_reason":"stop","done":true,"total_duration":812345678,"load_duration":4567890,"prompt_eval_count":26,"prompt_eval_duration":123456789,"eval_count":5,"eval_duration":456789012}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"model":"qwen3:8b","created_at":"2026-10-05T20:42:01.001Z","message":{"role":"assistant","content":"","thinking":"The user "},"done":false}
|
|
2
|
+
{"model":"qwen3:8b","created_at":"2026-10-05T20:42:01.031Z","message":{"role":"assistant","content":"","thinking":"wants a greeting."},"done":false}
|
|
3
|
+
{"model":"qwen3:8b","created_at":"2026-10-05T20:42:01.061Z","message":{"role":"assistant","content":"Hi!"},"done":false}
|
|
4
|
+
{"model":"qwen3:8b","created_at":"2026-10-05T20:42:01.091Z","message":{"role":"assistant","content":""},"done_reason":"stop","done":true,"prompt_eval_count":12,"eval_count":9}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"model":"granite4.1:3b","created_at":"2026-10-05T20:43:11.001Z","message":{"role":"assistant","content":"Checking."},"done":false}
|
|
2
|
+
{"model":"granite4.1:3b","created_at":"2026-10-05T20:43:11.031Z","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"read","arguments":{"path":"README.md"}}}]},"done":false}
|
|
3
|
+
{"model":"granite4.1:3b","created_at":"2026-10-05T20:43:11.061Z","message":{"role":"assistant","content":"","tool_calls":[{"id":"call_server7","function":{"index":1,"name":"bash","arguments":{"command":"ls -la","timeout":10}}}]},"done":false}
|
|
4
|
+
{"model":"granite4.1:3b","created_at":"2026-10-05T20:43:11.091Z","message":{"role":"assistant","content":""},"done_reason":"stop","done":true,"prompt_eval_count":210,"eval_count":44}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"model":"granite4.1:3b","created_at":"2026-10-05T20:45:01.001Z","message":{"role":"assistant","content":"Cut o"},"done":false}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"license":"Apache 2.0","modelfile":"# Modelfile","parameters":"stop \"<|end_of_text|>\"","template":"{{ .Prompt }}","details":{"parent_model":"","format":"gguf","family":"granite","families":["granite"],"parameter_size":"3.2B","quantization_level":"Q4_K_M"},"model_info":{"general.architecture":"granite","general.parameter_count":3188310016,"granite.context_length":131072},"capabilities":["completion","tools"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"details":{"family":"nomic-bert"},"model_info":{"general.architecture":"nomic-bert","nomic-bert.context_length":2048},"capabilities":["embedding"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"details":{"family":"qwen3","parameter_size":"8.2B"},"model_info":{"general.architecture":"qwen3","qwen3.context_length":40960},"capabilities":["completion","tools","thinking","vision"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"models":[{"name":"granite4.1:3b","model":"granite4.1:3b","modified_at":"2026-09-30T10:12:44.1234567-06:00","size":2013458432,"digest":"b1c2d3e4f5a60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90","details":{"parent_model":"","format":"gguf","family":"granite","families":["granite"],"parameter_size":"3.2B","quantization_level":"Q4_K_M"}},{"name":"qwen3:8b","model":"qwen3:8b","modified_at":"2026-09-28T08:01:02.5-06:00","size":5225388032,"digest":"0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90","details":{"parent_model":"","format":"gguf","family":"qwen3","families":["qwen3"],"parameter_size":"8.2B","quantization_level":"Q4_K_M"}},{"name":"nomic-embed-text:latest","model":"nomic-embed-text:latest","modified_at":"2026-09-01T08:01:02.5-06:00","size":274302450,"digest":"c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00","details":{"parent_model":"","format":"gguf","family":"nomic-bert","families":["nomic-bert"],"parameter_size":"137M","quantization_level":"F16"}}]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"models":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi-in-go/pigpen-ollama-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "PiG model provider (Go extension) for Ollama's native /api/chat, with the model list read from the server",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pig-package",
|
|
7
|
+
"pigpen",
|
|
8
|
+
"extension"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"author": "Michael Kinsy",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/MichaelKinsy/pigpen.git",
|
|
15
|
+
"directory": "components/ollama-native"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/MichaelKinsy/pigpen/tree/main/components/ollama-native#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/MichaelKinsy/pigpen/issues"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"extensions",
|
|
26
|
+
"README.md",
|
|
27
|
+
"CREDITS.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"provenance.json"
|
|
30
|
+
],
|
|
31
|
+
"contributors": [
|
|
32
|
+
"Peder Munksgaard"
|
|
33
|
+
],
|
|
34
|
+
"pi": {
|
|
35
|
+
"extensions": [
|
|
36
|
+
"extensions/ollama-native"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|