@eum-agent/llm-protocol-ollama 0.0.0-stage → 1.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/assets/NOTICE ADDED
@@ -0,0 +1,7 @@
1
+ Ollama mark: exact official SVG from https://github.com/ollama/ollama/blob/main/docs/logo.svg
2
+ Retrieved 2026-10-17. SHA-256: cfcdccd391bdff7c16b55eb9f6e13aa8b8bfe9f6e4a75a3a39783a6a1601c45f
3
+ Ollama trademarks remain the property of their respective owner.
4
+
5
+ Native API references: https://docs.ollama.com/api/chat and https://docs.ollama.com/capabilities/thinking
6
+ Experimental image profile: official v0.15.1 api/types.go, server/routes.go and x/imagegen/server.go at https://github.com/ollama/ollama/tree/v0.15.1
7
+ The release source proves the native image wire; model image capability, stable local digest and runtime service success remain required.
@@ -0,0 +1,3 @@
1
+ <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg"><title>Ollama</title>
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M7.25558 0.114339C7.61134 0.222519 7.93252 0.400698 8.22405 0.636149C8.70993 1.0256 9.12005 1.58303 9.433 2.24356C9.74758 2.90792 9.95182 3.64354 10.0292 4.38171C11.0662 3.9284 12.2171 3.65235 13.4041 3.57227L13.4881 3.56718C14.921 3.47809 16.3375 3.6779 17.5728 4.17044C17.7391 4.2379 17.9022 4.31044 18.062 4.3868C18.1443 3.66263 18.3453 2.94355 18.6549 2.29447C18.9678 1.63266 19.378 1.07651 19.8622 0.685785C20.1328 0.459579 20.4638 0.281532 20.8323 0.163974C21.2556 0.0367035 21.7053 0.0137947 22.1434 0.110521C22.8039 0.255609 23.3704 0.578877 23.8168 1.04851C24.2253 1.47739 24.5316 2.0272 24.7408 2.68646C25.1196 3.87517 25.1855 5.43933 24.9302 7.32549L25.0175 7.37639L25.0603 7.40058C26.3072 8.13366 27.1752 9.17855 27.6348 10.3914C28.3512 12.284 27.9905 14.4068 26.7552 15.5943L26.7255 15.621L26.7288 15.6248C27.4157 16.5946 27.8324 17.6192 27.9214 18.6793L27.9246 18.7175C28.0301 20.0729 27.5952 21.4373 26.5839 22.7774L26.5723 22.7902L26.5888 22.8207C27.3663 24.2932 27.6101 25.7759 27.3103 27.2574L27.3004 27.307C27.254 27.5234 27.0983 27.7168 26.8677 27.8446C26.637 27.9724 26.3501 28.0246 26.07 27.9892C25.9312 27.9724 25.7982 27.9347 25.6783 27.8782C25.5585 27.8217 25.4543 27.7474 25.3717 27.6595C25.289 27.572 25.2296 27.4725 25.1968 27.3668C25.164 27.2614 25.1585 27.152 25.1806 27.0448C25.4556 25.7301 25.197 24.4116 24.39 23.0702C24.3147 22.9456 24.2812 22.8083 24.2927 22.671C24.3043 22.5338 24.3604 22.401 24.4559 22.2849L24.4624 22.2773C25.4573 21.1013 25.869 19.9482 25.7801 18.8155C25.7043 17.8241 25.2448 16.8504 24.4624 15.9226C24.3103 15.7423 24.2561 15.5229 24.3115 15.3119C24.367 15.1009 24.5277 14.9152 24.7589 14.795L24.7737 14.7874C25.174 14.585 25.5429 14.0683 25.729 13.3619C25.9344 12.5267 25.8808 11.6658 25.5726 10.8496C25.2349 9.95872 24.6173 9.21546 23.7526 8.70765C22.7726 8.12984 21.4747 7.85111 19.8326 7.9313C19.6178 7.94209 19.4039 7.90286 19.2183 7.81869C19.0327 7.73451 18.8841 7.60927 18.7916 7.45912C18.2744 6.61277 17.5201 6.00696 16.5796 5.63151C15.6767 5.2833 14.6658 5.13696 13.661 5.20897C11.6104 5.33497 9.80194 6.22841 9.26335 7.35476C9.18715 7.51329 9.05009 7.65005 8.87052 7.74673C8.69096 7.84338 8.47747 7.89535 8.25864 7.89566C6.50122 7.8982 5.14075 8.21638 4.14592 8.79037C3.28615 9.28673 2.6998 9.98036 2.39015 10.8114C2.10995 11.5937 2.07158 12.4159 2.27815 13.2118C2.46262 13.9219 2.82333 14.5099 3.23674 14.8268L3.24992 14.8357C3.5991 15.0992 3.67321 15.5103 3.42945 15.8348C2.83651 16.6264 2.39345 17.8062 2.32098 18.9402C2.23862 20.2358 2.62733 21.3609 3.50521 22.1678L3.53157 22.192C3.66406 22.3113 3.74924 22.4576 3.77701 22.6133C3.80475 22.769 3.77385 22.9276 3.68804 23.0702C2.73933 24.6432 2.4478 25.9363 2.76239 26.9545C2.81892 27.1662 2.76631 27.3867 2.61573 27.5687C2.46516 27.7509 2.22851 27.8805 1.95615 27.9299C1.68379 27.9795 1.39724 27.9446 1.15746 27.8334C0.917644 27.7219 0.743586 27.5427 0.672268 27.3337C0.272031 26.0381 0.543797 24.5541 1.45133 22.8818L1.47438 22.8373L1.46121 22.822C1.01515 22.3129 0.682282 21.7498 0.476267 21.156L0.468032 21.1318C0.218008 20.391 0.119645 19.6244 0.176502 18.86C0.248972 17.7019 0.634385 16.5157 1.20097 15.5637L1.22074 15.5306L1.21744 15.5281C0.734856 14.9961 0.377443 14.3152 0.179796 13.5618L0.17156 13.5312C-0.100765 12.4803 -0.0482896 11.3945 0.324737 10.3622C0.756268 9.19764 1.6045 8.19729 2.85462 7.47439C2.95345 7.41712 3.05721 7.35985 3.16098 7.3064C2.89909 5.40624 2.96498 3.8319 3.34545 2.63556C3.55463 1.97629 3.86263 1.42648 4.2711 0.997598C4.71581 0.529242 5.2824 0.205974 5.94287 0.0596123C6.38099 -0.0371136 6.83228 -0.0142049 7.25558 0.114339ZM14.0349 11.6832C15.5765 11.6832 16.9996 12.0816 18.0636 12.7714C19.1013 13.4421 19.7189 14.3432 19.7189 15.2405C19.7189 16.3706 19.0502 17.2513 17.8528 17.8139C16.8316 18.2911 15.4629 18.5228 13.8949 18.5228C12.233 18.5228 10.8132 18.1931 9.78876 17.5886C8.77252 16.9904 8.20264 16.1504 8.20264 15.2405C8.20264 14.3407 8.85817 13.437 9.94194 12.7638C11.0422 12.0803 12.4949 11.6832 14.0349 11.6832ZM14.0349 12.8236C12.8922 12.8159 11.7798 13.1075 10.8791 13.6508C10.1198 14.1217 9.68994 14.7136 9.68994 15.2417C9.68994 15.7865 10.0358 16.2968 10.6946 16.685C11.4441 17.1266 12.5459 17.3824 13.8949 17.3824C15.2109 17.3824 16.321 17.1953 17.077 16.8403C17.8396 16.4839 18.23 15.9672 18.23 15.2405C18.23 14.7021 17.8248 14.1077 17.105 13.6419C16.3078 13.1265 15.2274 12.8236 14.0349 12.8236ZM15.1252 14.3636L15.1318 14.3687C15.3295 14.5608 15.2883 14.8396 15.0396 14.9923L14.5587 15.285V15.8526C14.5578 15.979 14.4921 16.0999 14.376 16.1889C14.2599 16.2779 14.1029 16.3277 13.9394 16.3274C13.7758 16.3277 13.6188 16.2779 13.5027 16.1889C13.3866 16.0999 13.3209 15.979 13.3201 15.8526V15.2672L12.8737 14.9897C12.8148 14.9533 12.7659 14.9082 12.7297 14.857C12.6935 14.8059 12.6707 14.7497 12.6628 14.6917C12.6548 14.6337 12.6618 14.5751 12.6833 14.5192C12.7048 14.4633 12.7404 14.4113 12.7881 14.3661C12.8853 14.2747 13.0253 14.2166 13.1776 14.2044C13.3299 14.1923 13.4824 14.2271 13.6017 14.3012L13.9558 14.5201L14.3182 14.2987C14.4371 14.2261 14.588 14.1922 14.7388 14.2043C14.8896 14.2165 15.0282 14.2736 15.1252 14.3636ZM6.82405 11.9212C7.61134 11.9212 8.25205 12.4176 8.25205 13.0298C8.25248 13.3232 8.10217 13.6048 7.83409 13.8127C7.56602 14.0205 7.20215 14.1376 6.8224 14.1383C6.44321 14.1373 6.08 14.0202 5.81235 13.8127C5.54467 13.6051 5.3944 13.324 5.3944 13.031C5.39351 12.7376 5.54342 12.4559 5.81117 12.2478C6.07895 12.0397 6.4443 11.9223 6.82405 11.9212ZM21.1634 11.9212C21.954 11.9212 22.593 12.4176 22.593 13.0298C22.5935 13.3232 22.4432 13.6048 22.1751 13.8127C21.907 14.0205 21.5431 14.1376 21.1634 14.1383C20.7842 14.1373 20.421 14.0202 20.1533 13.8127C19.8857 13.6051 19.7354 13.324 19.7354 13.031C19.7345 12.7376 19.8844 12.4559 20.1522 12.2478C20.4199 12.0397 20.7836 11.9223 21.1634 11.9212ZM6.48969 1.6543L6.48475 1.65684C6.29392 1.72096 6.131 1.82611 6.01534 1.95975L6.0071 1.96738C5.77981 2.20793 5.58216 2.56174 5.43393 3.02628C5.15392 3.90699 5.07816 5.10206 5.22969 6.56695C5.93793 6.40405 6.7104 6.30223 7.54217 6.26532L7.55864 6.26405L7.58993 6.22077C7.6657 6.11641 7.7464 6.01587 7.8337 5.9166C8.03629 4.93534 7.86993 3.76318 7.41699 2.8061C7.19628 2.34283 6.92781 1.97884 6.67087 1.77139C6.61783 1.72827 6.55871 1.68986 6.49463 1.65684L6.48969 1.6543ZM21.5999 1.70521L21.5966 1.70648C21.5325 1.73949 21.4734 1.7779 21.4203 1.82102C21.1634 2.02847 20.8933 2.39374 20.6742 2.85701C20.1966 3.86754 20.0368 5.11734 20.2954 6.13041L20.3909 6.25387L20.4041 6.27168H20.4535C21.2709 6.27186 22.0841 6.36273 22.8681 6.5415C23.0097 5.11097 22.9307 3.94136 22.6573 3.07719C22.509 2.61265 22.3114 2.25883 22.0824 2.01829L22.0759 2.01066C21.9604 1.87654 21.7975 1.77095 21.6064 1.70648L21.5999 1.70521Z" fill="black"/>
3
+ </svg>
package/dist/index.js ADDED
@@ -0,0 +1,865 @@
1
+ import { z } from "zod";
2
+ import { randomUUID } from "node:crypto";
3
+ //#region src/http.ts
4
+ /** Bounded transport shared by read-only discovery and native model streams. */
5
+ /** Validates a daemon root without credentials, query parameters or a resource suffix. */
6
+ function daemonRoot(value) {
7
+ if (typeof value !== "string" || value.length > 4096) throw new Error("Invalid Ollama Base URL");
8
+ const url = new URL(value);
9
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || /\/(?:api(?:\/(?:chat|generate|show|tags|version))?|v1)\/?$/.test(url.pathname)) throw new Error("Ollama Base URL must identify the daemon root");
10
+ return url.href.replace(/\/$/, "");
11
+ }
12
+ /** Reads one stream operation while observing cancellation even in transport doubles. */
13
+ async function abortable(work, signal) {
14
+ signal.throwIfAborted();
15
+ let abort = () => {};
16
+ const cancelled = new Promise((_, reject) => {
17
+ abort = () => reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
18
+ signal.addEventListener("abort", abort, { once: true });
19
+ });
20
+ try {
21
+ return await Promise.race([work, cancelled]);
22
+ } finally {
23
+ signal.removeEventListener("abort", abort);
24
+ }
25
+ }
26
+ /** HTTP failure contains a status only; daemon error text is never copied into diagnostics. */
27
+ var DaemonHttpError = class extends Error {
28
+ /** Response status reported by the daemon. */
29
+ status;
30
+ /** Records a refused response without retaining its headers or body. */
31
+ constructor(status) {
32
+ super("Ollama HTTP request failed");
33
+ this.status = status;
34
+ }
35
+ };
36
+ /** Fetches one native resource without authentication, redirects or ambient provider credentials. */
37
+ async function daemonFetch(fetch, root, resource, signal, body) {
38
+ const response = await abortable(fetch(`${root}/api/${resource}`, {
39
+ method: body === void 0 ? "GET" : "POST",
40
+ redirect: "error",
41
+ signal,
42
+ ...body === void 0 ? {} : {
43
+ headers: { "content-type": "application/json" },
44
+ body: JSON.stringify(body)
45
+ }
46
+ }), signal);
47
+ if (!response.ok) {
48
+ await response.body?.cancel().catch(() => {});
49
+ throw new DaemonHttpError(response.status);
50
+ }
51
+ return response;
52
+ }
53
+ /** Decodes a JSON response under the shared raw byte allowance before allocation or parsing. */
54
+ async function readJson(response, signal, budget) {
55
+ const reader = response.body?.getReader();
56
+ if (!reader) throw new Error("Ollama returned an empty body");
57
+ const chunks = [];
58
+ try {
59
+ while (true) {
60
+ const next = await abortable(reader.read(), signal);
61
+ if (next.done) break;
62
+ budget.remaining -= next.value.byteLength;
63
+ if (budget.remaining < 0) throw new Error("Ollama discovery exceeded its response budget");
64
+ chunks.push(next.value);
65
+ }
66
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)));
67
+ } finally {
68
+ await reader.cancel().catch(() => {});
69
+ reader.releaseLock();
70
+ }
71
+ }
72
+ /** Parses finite NDJSON, rejecting invalid UTF-8, oversize frames and unterminated streams. */
73
+ async function* readFrames(response, signal, maxBytes = 67108864, frameBytes = 8388608) {
74
+ const reader = response.body?.getReader();
75
+ if (!reader) throw new Error("Ollama returned an empty stream");
76
+ const decoder = new TextDecoder("utf-8", { fatal: true });
77
+ let pending = "";
78
+ let bytes = 0;
79
+ try {
80
+ while (true) {
81
+ const next = await abortable(reader.read(), signal);
82
+ if (next.done) break;
83
+ bytes += next.value.byteLength;
84
+ if (bytes > maxBytes) throw new Error("Ollama stream exceeded its response budget");
85
+ pending += decoder.decode(next.value, { stream: true });
86
+ let index = pending.indexOf("\n");
87
+ while (index >= 0) {
88
+ const line = pending.slice(0, index).trim();
89
+ if (Buffer.byteLength(line) > frameBytes) throw new Error("Ollama stream frame exceeded its size limit");
90
+ pending = pending.slice(index + 1);
91
+ if (line) yield JSON.parse(line);
92
+ index = pending.indexOf("\n");
93
+ }
94
+ if (Buffer.byteLength(pending) > frameBytes) throw new Error("Ollama stream frame exceeded its size limit");
95
+ }
96
+ pending += decoder.decode();
97
+ if (pending.trim()) yield JSON.parse(pending);
98
+ } finally {
99
+ await reader.cancel().catch(() => {});
100
+ reader.releaseLock();
101
+ }
102
+ }
103
+ //#endregion
104
+ //#region src/discovery.ts
105
+ const thinkingSchema = z.object({
106
+ values: z.array(z.union([z.boolean(), z.string().min(1).max(64)])).max(32),
107
+ default: z.union([z.boolean(), z.string()]).optional()
108
+ });
109
+ const tagSchema = z.object({
110
+ name: z.string().min(1).max(512),
111
+ digest: z.string().regex(/^(?:sha256:)?[a-f0-9]{64}$/),
112
+ remote_model: z.string().optional(),
113
+ remote_host: z.string().optional()
114
+ });
115
+ const tagsSchema = z.object({ models: z.array(tagSchema).max(1e3) });
116
+ const showSchema = z.object({
117
+ capabilities: z.array(z.string()).max(64).optional(),
118
+ thinking: thinkingSchema.optional(),
119
+ model_info: z.record(z.string(), z.unknown()).optional(),
120
+ remote_model: z.string().optional(),
121
+ remote_host: z.string().optional()
122
+ });
123
+ const evidenceRow = z.strictObject({
124
+ model: z.string(),
125
+ digest: tagSchema.shape.digest,
126
+ capabilities: z.array(z.string()).optional(),
127
+ thinking: thinkingSchema.optional(),
128
+ remote: z.boolean(),
129
+ stable: z.boolean()
130
+ });
131
+ const evidenceSchema = z.strictObject({
132
+ baseUrl: z.string(),
133
+ version: z.string(),
134
+ models: z.array(evidenceRow).max(1e3)
135
+ });
136
+ /** Exact experimental native image wire verified against the official release source. */
137
+ const IMAGE_PROFILE = "image-v1:0.15.1";
138
+ function imageProfile(version, capabilities) {
139
+ return version === "0.15.1" && capabilities?.includes("image") ? IMAGE_PROFILE : void 0;
140
+ }
141
+ /** Creates a compact protocol-owned profile from exact model capabilities. */
142
+ function profileOf(capabilities, thinking) {
143
+ if (!capabilities?.includes("completion")) return void 0;
144
+ const flags = `${capabilities.includes("vision") ? 1 : 0}${capabilities.includes("tools") ? 1 : 0}${capabilities.includes("thinking") ? 1 : 0}`;
145
+ const values = thinking?.values ?? [];
146
+ return `chat-v1:${flags}:${Buffer.from(JSON.stringify(values)).toString("base64url")}`;
147
+ }
148
+ /** Validates a saved conversation profile without querying mutable discovery caches. */
149
+ function parseChatProfile(profile) {
150
+ const match = /^chat-v1:([01])([01])([01]):([A-Za-z0-9_-]+)$/.exec(profile);
151
+ if (!match) return void 0;
152
+ try {
153
+ const values = thinkingSchema.shape.values.parse(JSON.parse(Buffer.from(match[4] ?? "", "base64url").toString("utf8")));
154
+ if (Buffer.from(JSON.stringify(values)).toString("base64url") !== match[4]) return void 0;
155
+ return {
156
+ vision: match[1] === "1",
157
+ tools: match[2] === "1",
158
+ reasoning: match[3] === "1",
159
+ thinking: values
160
+ };
161
+ } catch {
162
+ return;
163
+ }
164
+ }
165
+ function remote(row) {
166
+ return Boolean(row.remote_model || row.remote_host);
167
+ }
168
+ /** Discovers installed local weights with bounded show concurrency and before/after tag identity. */
169
+ async function discoverModels(probe, fetch) {
170
+ const signal = AbortSignal.any([probe.signal, AbortSignal.timeout(3e4)]);
171
+ const budget = { remaining: Math.min(probe.maxResponseBytes ?? 8388608, 8388608) };
172
+ const root = daemonRoot(probe.config.baseUrl);
173
+ const get = async (resource, body) => readJson(await daemonFetch(fetch, root, resource, signal, body), signal, budget);
174
+ const version = z.object({ version: z.string().min(1).max(128) }).parse(await get("version")).version;
175
+ const tags = tagsSchema.parse(await get("tags")).models;
176
+ if (new Set(tags.map((tag) => tag.name)).size !== tags.length) throw new Error("Ollama returned duplicate model tags");
177
+ const selected = probe.model ? tags.filter((tag) => tag.name === probe.model) : tags;
178
+ const details = /* @__PURE__ */ new Map();
179
+ let index = 0;
180
+ await Promise.all(Array.from({ length: Math.min(4, selected.length) }, async () => {
181
+ while (index < selected.length) {
182
+ const tag = selected[index++];
183
+ if (!tag || remote(tag)) continue;
184
+ try {
185
+ details.set(tag.name, showSchema.parse(await get("show", { model: tag.name })));
186
+ } catch {
187
+ signal.throwIfAborted();
188
+ if (budget.remaining < 0) throw new Error("Ollama discovery exceeded its response budget");
189
+ }
190
+ }
191
+ }));
192
+ const after = tagsSchema.parse(await get("tags")).models;
193
+ const evidence = {
194
+ baseUrl: root,
195
+ version,
196
+ models: []
197
+ };
198
+ return {
199
+ offers: selected.map((tag) => {
200
+ const show = details.get(tag.name);
201
+ const current = after.find((row) => row.name === tag.name);
202
+ const stable = current?.digest === tag.digest;
203
+ const isRemote = remote(tag) || Boolean(show && remote(show)) || Boolean(current && remote(current));
204
+ const profile = stable && !isRemote && show ? profileOf(show.capabilities, show.thinking) ?? imageProfile(version, show.capabilities) : void 0;
205
+ evidence.models.push({
206
+ model: tag.name,
207
+ digest: tag.digest,
208
+ capabilities: show?.capabilities,
209
+ thinking: show?.thinking,
210
+ remote: isRemote,
211
+ stable
212
+ });
213
+ const context = Object.entries(show?.model_info ?? {}).filter(([key, value]) => key.endsWith(".context_length") && Number.isSafeInteger(value) && Number(value) > 0).map(([, value]) => Number(value));
214
+ return {
215
+ model: tag.name,
216
+ ...context.length ? { contextWindow: Math.min(...context) } : {},
217
+ ...show?.capabilities ? {
218
+ input: isRemote ? [] : ["text", ...show.capabilities.includes("vision") ? ["image"] : []],
219
+ output: isRemote ? [] : show.capabilities.includes("completion") ? ["text"] : show.capabilities.includes("image") ? ["image"] : [],
220
+ reasoning: show.capabilities.includes("thinking"),
221
+ toolCall: !isRemote && show.capabilities.includes("tools"),
222
+ temperature: true
223
+ } : {},
224
+ ...show?.thinking?.values.some((value) => typeof value === "string") ? {
225
+ efforts: show.thinking.values.filter((value) => typeof value === "string"),
226
+ ...typeof show.thinking.default === "string" ? { defaultEffort: show.thinking.default } : {}
227
+ } : {},
228
+ execution: profile ? [{
229
+ profile,
230
+ chat: profile !== IMAGE_PROFILE,
231
+ ...profile === "image-v1:0.15.1" ? { generation: ["image"] } : {}
232
+ }] : []
233
+ };
234
+ }),
235
+ evidence
236
+ };
237
+ }
238
+ /** Accepts only the exact local installed identity and capability profile from this host listing. */
239
+ function validateExecution(input) {
240
+ const evidence = evidenceSchema.parse(input.evidence);
241
+ const row = evidence.models.find((model) => model.model === input.model);
242
+ const profile = row && !row.remote && row.stable ? profileOf(row.capabilities, row.thinking) ?? imageProfile(evidence.version, row.capabilities) : void 0;
243
+ if (evidence.baseUrl !== daemonRoot(input.config.baseUrl) || input.requested.protocolId !== "ollama" || !row || !profile || profile !== input.requested.profile || input.requested.sourceIdentity !== void 0 && input.requested.sourceIdentity !== row.digest) throw new Error("Refresh and save an installed local Ollama model before execution");
244
+ return {
245
+ protocolId: "ollama",
246
+ profile,
247
+ sourceIdentity: row.digest
248
+ };
249
+ }
250
+ /** Rechecks tags and details immediately before inference; cloud placeholders never execute. */
251
+ async function revalidateModel(probe, fetch, selection) {
252
+ const listing = await discoverModels(probe, fetch);
253
+ validateExecution({
254
+ model: probe.model ?? "",
255
+ requested: selection,
256
+ config: probe.config,
257
+ evidence: listing.evidence
258
+ });
259
+ }
260
+ //#endregion
261
+ //#region src/image.ts
262
+ /** Validated square generation sizes; dimensions remain below the daemon's 4096-pixel ceiling. */
263
+ const IMAGE_PARAMS = z.strictObject({
264
+ width: z.literal(1024).default(1024),
265
+ height: z.literal(1024).default(1024),
266
+ steps: z.number().int().min(1).max(50).default(9),
267
+ seed: z.number().int().min(0).max(2147483647).optional()
268
+ });
269
+ /** Declares only the create operation proved by the selected image adapter. */
270
+ function imageModel(id) {
271
+ return {
272
+ id,
273
+ label: id,
274
+ outputKinds: ["image"],
275
+ operations: [{
276
+ id: "create",
277
+ inputKinds: [],
278
+ minInputs: 0,
279
+ maxInputs: 0,
280
+ paramsSchema: IMAGE_PARAMS
281
+ }]
282
+ };
283
+ }
284
+ /** Normalized experimental generation failure; raw daemon text never leaves this adapter. */
285
+ var ImageFailure = class extends Error {
286
+ /** Cross-realm discriminator recognized by the host. */
287
+ name = "GenerationFailure";
288
+ /** Host failure category. */
289
+ code;
290
+ /** Whether a submission may already have executed. */
291
+ resultState;
292
+ /** Records bounded fixed failure text and submission knowledge. */
293
+ constructor(code, message, resultState) {
294
+ super(message);
295
+ this.code = code;
296
+ this.resultState = resultState;
297
+ }
298
+ };
299
+ const imageFrame = z.object({
300
+ error: z.string().optional(),
301
+ image: z.string().optional(),
302
+ done: z.boolean().optional(),
303
+ completed: z.number().int().nonnegative().optional(),
304
+ total: z.number().int().positive().optional()
305
+ });
306
+ /** Submits a single experimental image request after rechecking the installed model and daemon. */
307
+ function createImagePort(call, fetch) {
308
+ return { async generate(request, signal) {
309
+ const execution = call.configuredModels.find((row) => row.id === call.model)?.execution;
310
+ if (!execution || execution.protocolId !== "ollama" || execution.profile !== "image-v1:0.15.1" || !execution.sourceIdentity) throw new ImageFailure("unavailable", "Refresh and save a supported Ollama image model.", "not-submitted");
311
+ if (request.operation !== "create" || request.outputKind !== "image" || request.inputs.length) throw new ImageFailure("unsupported", "This Ollama image adapter supports creation without reference images; editing is unavailable.", "not-submitted");
312
+ const params = IMAGE_PARAMS.safeParse(request.params);
313
+ if (!params.success) throw new ImageFailure("invalid-params", "Unsupported Ollama image parameters.", "not-submitted");
314
+ const prompt = request.prompt.trim();
315
+ if (![...prompt].length || [...prompt].length > 32e3) throw new ImageFailure("invalid-input", "Image prompt must contain 1–32000 characters.", "not-submitted");
316
+ signal.throwIfAborted();
317
+ try {
318
+ await revalidateModel({
319
+ ...call,
320
+ signal,
321
+ model: call.model
322
+ }, fetch, execution);
323
+ } catch {
324
+ if (signal.aborted) throw new ImageFailure("cancelled", "Image generation was cancelled.", "not-submitted");
325
+ throw new ImageFailure("unavailable", "The installed Ollama image model or service changed; refresh and save it again.", "not-submitted");
326
+ }
327
+ const timeout = new AbortController();
328
+ const active = AbortSignal.any([
329
+ signal,
330
+ timeout.signal,
331
+ AbortSignal.timeout(9e5)
332
+ ]);
333
+ let timer = setTimeout(() => timeout.abort(/* @__PURE__ */ new Error("Ollama image response timed out")), 3e5);
334
+ let submitted = false;
335
+ try {
336
+ active.throwIfAborted();
337
+ submitted = true;
338
+ const response = await daemonFetch(fetch, daemonRoot(call.config.baseUrl), "generate", active, {
339
+ model: call.model,
340
+ prompt,
341
+ stream: true,
342
+ width: params.data.width,
343
+ height: params.data.height,
344
+ steps: params.data.steps,
345
+ ...params.data.seed === void 0 ? {} : { options: { seed: params.data.seed } }
346
+ });
347
+ let image;
348
+ let done = false;
349
+ let completed = 0;
350
+ let total;
351
+ for await (const raw of readFrames(response, active, 134217728, 100663296)) {
352
+ clearTimeout(timer);
353
+ timer = setTimeout(() => timeout.abort(/* @__PURE__ */ new Error("Ollama image stream stalled")), 6e4);
354
+ if (done) throw new ImageFailure("invalid-output", "Ollama returned image data after completion.", "not-delivered");
355
+ const frame = imageFrame.parse(raw);
356
+ if (frame.error) throw new ImageFailure("provider", "Ollama rejected the image request.", "not-delivered");
357
+ if (frame.completed !== void 0) {
358
+ if (frame.completed < completed || frame.total !== void 0 && frame.completed > frame.total) throw new ImageFailure("invalid-output", "Ollama image progress was invalid.", "not-delivered");
359
+ completed = frame.completed;
360
+ }
361
+ if (frame.total !== void 0) {
362
+ if (total !== void 0 && total !== frame.total) throw new ImageFailure("invalid-output", "Ollama image progress changed its total.", "not-delivered");
363
+ total = frame.total;
364
+ }
365
+ if (frame.image !== void 0) {
366
+ if (image || frame.image.length > Math.ceil(67108864 / 3) * 4 || /[^A-Za-z0-9+/=]/.test(frame.image)) throw new ImageFailure("invalid-output", "Ollama returned an invalid singular image.", "not-delivered");
367
+ image = Buffer.from(frame.image, "base64");
368
+ if (!image.length || image.length > 67108864 || Buffer.from(image).toString("base64") !== frame.image) throw new ImageFailure("too-large", "Ollama image exceeded the byte limit.", "not-delivered");
369
+ }
370
+ if (frame.done) done = true;
371
+ }
372
+ if (!done || !image) throw new ImageFailure("invalid-output", "Ollama did not complete an image response.", done ? "not-delivered" : "unknown");
373
+ const bytes = image;
374
+ return { assets: [{
375
+ mediaType: "image/png",
376
+ name: "ollama-image.png",
377
+ body: (async function* () {
378
+ yield bytes;
379
+ })()
380
+ }] };
381
+ } catch (error) {
382
+ if (error instanceof ImageFailure) throw error;
383
+ if (active.aborted) throw new ImageFailure(signal.aborted ? "cancelled" : "timeout", "Ollama image generation stopped; the daemon may still be computing.", submitted ? "unknown" : "not-submitted");
384
+ if (error instanceof DaemonHttpError) throw new ImageFailure(error.status === 429 ? "quota" : "provider", "Ollama image request failed.", "not-delivered");
385
+ throw new ImageFailure("invalid-output", "Ollama returned an unsupported image stream.", submitted ? "unknown" : "not-submitted");
386
+ } finally {
387
+ clearTimeout(timer);
388
+ }
389
+ } };
390
+ }
391
+ //#endregion
392
+ //#region src/logo.ts
393
+ /** Self-contained official Ollama mark; provenance is shipped in assets/NOTICE. */
394
+ const OLLAMA_LOGO = "data:image/svg+xml,%3Csvg%20width%3D%2228%22%20height%3D%2228%22%20viewBox%3D%220%200%2028%2028%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M7.25558%200.114339C7.61134%200.222519%207.93252%200.400698%208.22405%200.636149C8.70993%201.0256%209.12005%201.58303%209.433%202.24356C9.74758%202.90792%209.95182%203.64354%2010.0292%204.38171C11.0662%203.9284%2012.2171%203.65235%2013.4041%203.57227L13.4881%203.56718C14.921%203.47809%2016.3375%203.6779%2017.5728%204.17044C17.7391%204.2379%2017.9022%204.31044%2018.062%204.3868C18.1443%203.66263%2018.3453%202.94355%2018.6549%202.29447C18.9678%201.63266%2019.378%201.07651%2019.8622%200.685785C20.1328%200.459579%2020.4638%200.281532%2020.8323%200.163974C21.2556%200.0367035%2021.7053%200.0137947%2022.1434%200.110521C22.8039%200.255609%2023.3704%200.578877%2023.8168%201.04851C24.2253%201.47739%2024.5316%202.0272%2024.7408%202.68646C25.1196%203.87517%2025.1855%205.43933%2024.9302%207.32549L25.0175%207.37639L25.0603%207.40058C26.3072%208.13366%2027.1752%209.17855%2027.6348%2010.3914C28.3512%2012.284%2027.9905%2014.4068%2026.7552%2015.5943L26.7255%2015.621L26.7288%2015.6248C27.4157%2016.5946%2027.8324%2017.6192%2027.9214%2018.6793L27.9246%2018.7175C28.0301%2020.0729%2027.5952%2021.4373%2026.5839%2022.7774L26.5723%2022.7902L26.5888%2022.8207C27.3663%2024.2932%2027.6101%2025.7759%2027.3103%2027.2574L27.3004%2027.307C27.254%2027.5234%2027.0983%2027.7168%2026.8677%2027.8446C26.637%2027.9724%2026.3501%2028.0246%2026.07%2027.9892C25.9312%2027.9724%2025.7982%2027.9347%2025.6783%2027.8782C25.5585%2027.8217%2025.4543%2027.7474%2025.3717%2027.6595C25.289%2027.572%2025.2296%2027.4725%2025.1968%2027.3668C25.164%2027.2614%2025.1585%2027.152%2025.1806%2027.0448C25.4556%2025.7301%2025.197%2024.4116%2024.39%2023.0702C24.3147%2022.9456%2024.2812%2022.8083%2024.2927%2022.671C24.3043%2022.5338%2024.3604%2022.401%2024.4559%2022.2849L24.4624%2022.2773C25.4573%2021.1013%2025.869%2019.9482%2025.7801%2018.8155C25.7043%2017.8241%2025.2448%2016.8504%2024.4624%2015.9226C24.3103%2015.7423%2024.2561%2015.5229%2024.3115%2015.3119C24.367%2015.1009%2024.5277%2014.9152%2024.7589%2014.795L24.7737%2014.7874C25.174%2014.585%2025.5429%2014.0683%2025.729%2013.3619C25.9344%2012.5267%2025.8808%2011.6658%2025.5726%2010.8496C25.2349%209.95872%2024.6173%209.21546%2023.7526%208.70765C22.7726%208.12984%2021.4747%207.85111%2019.8326%207.9313C19.6178%207.94209%2019.4039%207.90286%2019.2183%207.81869C19.0327%207.73451%2018.8841%207.60927%2018.7916%207.45912C18.2744%206.61277%2017.5201%206.00696%2016.5796%205.63151C15.6767%205.2833%2014.6658%205.13696%2013.661%205.20897C11.6104%205.33497%209.80194%206.22841%209.26335%207.35476C9.18715%207.51329%209.05009%207.65005%208.87052%207.74673C8.69096%207.84338%208.47747%207.89535%208.25864%207.89566C6.50122%207.8982%205.14075%208.21638%204.14592%208.79037C3.28615%209.28673%202.6998%209.98036%202.39015%2010.8114C2.10995%2011.5937%202.07158%2012.4159%202.27815%2013.2118C2.46262%2013.9219%202.82333%2014.5099%203.23674%2014.8268L3.24992%2014.8357C3.5991%2015.0992%203.67321%2015.5103%203.42945%2015.8348C2.83651%2016.6264%202.39345%2017.8062%202.32098%2018.9402C2.23862%2020.2358%202.62733%2021.3609%203.50521%2022.1678L3.53157%2022.192C3.66406%2022.3113%203.74924%2022.4576%203.77701%2022.6133C3.80475%2022.769%203.77385%2022.9276%203.68804%2023.0702C2.73933%2024.6432%202.4478%2025.9363%202.76239%2026.9545C2.81892%2027.1662%202.76631%2027.3867%202.61573%2027.5687C2.46516%2027.7509%202.22851%2027.8805%201.95615%2027.9299C1.68379%2027.9795%201.39724%2027.9446%201.15746%2027.8334C0.917644%2027.7219%200.743586%2027.5427%200.672268%2027.3337C0.272031%2026.0381%200.543797%2024.5541%201.45133%2022.8818L1.47438%2022.8373L1.46121%2022.822C1.01515%2022.3129%200.682282%2021.7498%200.476267%2021.156L0.468032%2021.1318C0.218008%2020.391%200.119645%2019.6244%200.176502%2018.86C0.248972%2017.7019%200.634385%2016.5157%201.20097%2015.5637L1.22074%2015.5306L1.21744%2015.5281C0.734856%2014.9961%200.377443%2014.3152%200.179796%2013.5618L0.17156%2013.5312C-0.100765%2012.4803%20-0.0482896%2011.3945%200.324737%2010.3622C0.756268%209.19764%201.6045%208.19729%202.85462%207.47439C2.95345%207.41712%203.05721%207.35985%203.16098%207.3064C2.89909%205.40624%202.96498%203.8319%203.34545%202.63556C3.55463%201.97629%203.86263%201.42648%204.2711%200.997598C4.71581%200.529242%205.2824%200.205974%205.94287%200.0596123C6.38099%20-0.0371136%206.83228%20-0.0142049%207.25558%200.114339ZM14.0349%2011.6832C15.5765%2011.6832%2016.9996%2012.0816%2018.0636%2012.7714C19.1013%2013.4421%2019.7189%2014.3432%2019.7189%2015.2405C19.7189%2016.3706%2019.0502%2017.2513%2017.8528%2017.8139C16.8316%2018.2911%2015.4629%2018.5228%2013.8949%2018.5228C12.233%2018.5228%2010.8132%2018.1931%209.78876%2017.5886C8.77252%2016.9904%208.20264%2016.1504%208.20264%2015.2405C8.20264%2014.3407%208.85817%2013.437%209.94194%2012.7638C11.0422%2012.0803%2012.4949%2011.6832%2014.0349%2011.6832ZM14.0349%2012.8236C12.8922%2012.8159%2011.7798%2013.1075%2010.8791%2013.6508C10.1198%2014.1217%209.68994%2014.7136%209.68994%2015.2417C9.68994%2015.7865%2010.0358%2016.2968%2010.6946%2016.685C11.4441%2017.1266%2012.5459%2017.3824%2013.8949%2017.3824C15.2109%2017.3824%2016.321%2017.1953%2017.077%2016.8403C17.8396%2016.4839%2018.23%2015.9672%2018.23%2015.2405C18.23%2014.7021%2017.8248%2014.1077%2017.105%2013.6419C16.3078%2013.1265%2015.2274%2012.8236%2014.0349%2012.8236ZM15.1252%2014.3636L15.1318%2014.3687C15.3295%2014.5608%2015.2883%2014.8396%2015.0396%2014.9923L14.5587%2015.285V15.8526C14.5578%2015.979%2014.4921%2016.0999%2014.376%2016.1889C14.2599%2016.2779%2014.1029%2016.3277%2013.9394%2016.3274C13.7758%2016.3277%2013.6188%2016.2779%2013.5027%2016.1889C13.3866%2016.0999%2013.3209%2015.979%2013.3201%2015.8526V15.2672L12.8737%2014.9897C12.8148%2014.9533%2012.7659%2014.9082%2012.7297%2014.857C12.6935%2014.8059%2012.6707%2014.7497%2012.6628%2014.6917C12.6548%2014.6337%2012.6618%2014.5751%2012.6833%2014.5192C12.7048%2014.4633%2012.7404%2014.4113%2012.7881%2014.3661C12.8853%2014.2747%2013.0253%2014.2166%2013.1776%2014.2044C13.3299%2014.1923%2013.4824%2014.2271%2013.6017%2014.3012L13.9558%2014.5201L14.3182%2014.2987C14.4371%2014.2261%2014.588%2014.1922%2014.7388%2014.2043C14.8896%2014.2165%2015.0282%2014.2736%2015.1252%2014.3636ZM6.82405%2011.9212C7.61134%2011.9212%208.25205%2012.4176%208.25205%2013.0298C8.25248%2013.3232%208.10217%2013.6048%207.83409%2013.8127C7.56602%2014.0205%207.20215%2014.1376%206.8224%2014.1383C6.44321%2014.1373%206.08%2014.0202%205.81235%2013.8127C5.54467%2013.6051%205.3944%2013.324%205.3944%2013.031C5.39351%2012.7376%205.54342%2012.4559%205.81117%2012.2478C6.07895%2012.0397%206.4443%2011.9223%206.82405%2011.9212ZM21.1634%2011.9212C21.954%2011.9212%2022.593%2012.4176%2022.593%2013.0298C22.5935%2013.3232%2022.4432%2013.6048%2022.1751%2013.8127C21.907%2014.0205%2021.5431%2014.1376%2021.1634%2014.1383C20.7842%2014.1373%2020.421%2014.0202%2020.1533%2013.8127C19.8857%2013.6051%2019.7354%2013.324%2019.7354%2013.031C19.7345%2012.7376%2019.8844%2012.4559%2020.1522%2012.2478C20.4199%2012.0397%2020.7836%2011.9223%2021.1634%2011.9212ZM6.48969%201.6543L6.48475%201.65684C6.29392%201.72096%206.131%201.82611%206.01534%201.95975L6.0071%201.96738C5.77981%202.20793%205.58216%202.56174%205.43393%203.02628C5.15392%203.90699%205.07816%205.10206%205.22969%206.56695C5.93793%206.40405%206.7104%206.30223%207.54217%206.26532L7.55864%206.26405L7.58993%206.22077C7.6657%206.11641%207.7464%206.01587%207.8337%205.9166C8.03629%204.93534%207.86993%203.76318%207.41699%202.8061C7.19628%202.34283%206.92781%201.97884%206.67087%201.77139C6.61783%201.72827%206.55871%201.68986%206.49463%201.65684L6.48969%201.6543ZM21.5999%201.70521L21.5966%201.70648C21.5325%201.73949%2021.4734%201.7779%2021.4203%201.82102C21.1634%202.02847%2020.8933%202.39374%2020.6742%202.85701C20.1966%203.86754%2020.0368%205.11734%2020.2954%206.13041L20.3909%206.25387L20.4041%206.27168H20.4535C21.2709%206.27186%2022.0841%206.36273%2022.8681%206.5415C23.0097%205.11097%2022.9307%203.94136%2022.6573%203.07719C22.509%202.61265%2022.3114%202.25883%2022.0824%202.01829L22.0759%202.01066C21.9604%201.87654%2021.7975%201.77095%2021.6064%201.70648L21.5999%201.70521Z%22%20fill%3D%22black%22%2F%3E%0A%3C%2Fsvg%3E%0A";
395
+ //#endregion
396
+ //#region src/port.ts
397
+ /** Native Ollama conversation request rendering, measurement and streaming. */
398
+ const settingsSchema = z.strictObject({
399
+ maxTokens: z.number().int().positive().max(1e6).optional(),
400
+ temperature: z.number().min(0).max(2).optional(),
401
+ contextWindow: z.number().int().positive().max(1e7).optional(),
402
+ thinking: z.boolean().optional(),
403
+ effort: z.string().max(64).optional(),
404
+ effortCache: z.boolean().optional(),
405
+ efforts: z.array(z.object({
406
+ value: z.string(),
407
+ label: z.string().optional()
408
+ })).optional()
409
+ });
410
+ const toolCallSchema = z.object({
411
+ id: z.string().min(1).max(512).optional(),
412
+ function: z.object({
413
+ index: z.number().int().nonnegative().optional(),
414
+ name: z.string().min(1).max(256),
415
+ arguments: z.record(z.string(), z.unknown())
416
+ })
417
+ });
418
+ const frameSchema = z.object({
419
+ error: z.string().optional(),
420
+ done: z.boolean().optional(),
421
+ done_reason: z.string().optional(),
422
+ message: z.object({
423
+ content: z.string().optional(),
424
+ thinking: z.string().optional(),
425
+ tool_calls: z.array(toolCallSchema).max(128).optional()
426
+ }).optional(),
427
+ prompt_eval_count: z.number().int().nonnegative().optional(),
428
+ eval_count: z.number().int().nonnegative().optional(),
429
+ prompt_eval_cached_count: z.number().int().nonnegative().optional()
430
+ });
431
+ /**
432
+ * Creates a native conversation port that measures oversized candidates without IO.
433
+ * Submissions enforce context capacity and recheck the saved weight identity.
434
+ */
435
+ function createOllamaPort(call, support, fetch) {
436
+ const selection = call.configuredModels.find((row) => row.id === call.model)?.execution;
437
+ const profile = selection?.protocolId === "ollama" ? parseChatProfile(selection.profile) : void 0;
438
+ const root = daemonRoot(call.config.baseUrl);
439
+ function build(request) {
440
+ if (!selection || !profile || !selection.sourceIdentity) throw new Error("Refresh and save an installed Ollama conversation model");
441
+ const params = settingsSchema.parse(request.params ?? {});
442
+ const catalogue = support.catalogue(call.providerId, call.model);
443
+ const context = params.contextWindow ?? Math.min(catalogue?.limit?.context ?? 8192, 8192);
444
+ if (catalogue?.limit?.context && context > catalogue.limit.context) throw new Error("Requested Ollama context exceeds model capacity");
445
+ const maxTokens = request.purpose === "summary" ? request.summaryMaxTokens : params.maxTokens ?? 1024;
446
+ if (!Number.isSafeInteger(maxTokens) || Number(maxTokens) <= 0 || Number(maxTokens) > context) throw new Error("Invalid Ollama output token allowance");
447
+ const media = support.planMedia(request, {
448
+ acceptsImages: profile.vision,
449
+ limits: {
450
+ requestBytes: 20971520,
451
+ images: 50
452
+ }
453
+ });
454
+ const messages = [];
455
+ if (request.system?.length) messages.push({
456
+ role: "system",
457
+ content: request.system.join("\n\n")
458
+ });
459
+ const notes = (values) => {
460
+ const text = (values ?? []).map((note) => note.text).join("\n\n");
461
+ if (text) messages.push({
462
+ role: "user",
463
+ content: text
464
+ });
465
+ };
466
+ const names = /* @__PURE__ */ new Map();
467
+ request.messages.forEach((message, messageIndex) => {
468
+ if (message.role === "assistant") {
469
+ notes(message.volatile);
470
+ const calls = message.content.filter((part) => part.type === "tool-use");
471
+ for (const call of calls) names.set(call.id, call.name);
472
+ const text = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
473
+ const thinking = message.outputTruncation?.incompleteThinking ? "" : message.content.filter((part) => part.type === "thinking").map((part) => part.text).join("");
474
+ if (text || calls.length) messages.push({
475
+ role: "assistant",
476
+ content: text,
477
+ ...thinking ? { thinking } : {},
478
+ ...calls.length ? { tool_calls: calls.map((call) => ({
479
+ id: call.id,
480
+ function: {
481
+ name: call.name,
482
+ arguments: call.input
483
+ }
484
+ })) } : {}
485
+ });
486
+ return;
487
+ }
488
+ const lines = [];
489
+ const images = [];
490
+ message.content.forEach((part, partIndex) => {
491
+ if (part.type === "text") lines.push(part.text);
492
+ else if (part.type === "file") {
493
+ const planned = media.at(messageIndex, partIndex);
494
+ lines.push(planned.label);
495
+ if (planned.fate === "send" && planned.data !== void 0) images.push(planned.data);
496
+ } else if (part.type === "tool-result") {
497
+ const name = names.get(part.toolUseId);
498
+ if (!name) throw new Error("Ollama tool result has no matching preceding call");
499
+ const result = [part.content];
500
+ part.files?.forEach((_, fileIndex) => {
501
+ const planned = media.at(messageIndex, partIndex, fileIndex);
502
+ if (planned.fate === "send") {
503
+ lines.push(`${part.toolUseId} ${planned.label}`);
504
+ if (planned.data !== void 0) images.push(planned.data);
505
+ } else result.push(planned.label);
506
+ });
507
+ messages.push({
508
+ role: "tool",
509
+ content: result.join("\n"),
510
+ tool_name: name,
511
+ tool_call_id: part.toolUseId
512
+ });
513
+ }
514
+ });
515
+ if (lines.length || images.length) messages.push({
516
+ role: "user",
517
+ content: lines.join("\n"),
518
+ ...images.length ? { images } : {}
519
+ });
520
+ });
521
+ notes(request.volatile);
522
+ let think;
523
+ if (params.effort && params.effort !== "none") {
524
+ if (!profile.thinking.includes(params.effort)) throw new Error("Unsupported Ollama thinking effort");
525
+ think = params.effort;
526
+ }
527
+ if (params.thinking !== void 0 || params.effort === "none") {
528
+ const enabled = params.thinking !== false && params.effort !== "none";
529
+ if (!enabled || think === void 0) {
530
+ if (profile.thinking.includes(enabled)) think = enabled;
531
+ else if (!(enabled && profile.reasoning && (!profile.thinking.length || profile.thinking.some((value) => typeof value === "string")))) throw new Error("Unsupported Ollama thinking switch");
532
+ }
533
+ }
534
+ const tools = request.toolChoice === "none" ? [] : request.tools ?? [];
535
+ if (tools.length && !profile.tools) throw new Error("This Ollama model does not support tools");
536
+ const body = {
537
+ model: call.model,
538
+ stream: true,
539
+ truncate: false,
540
+ messages,
541
+ ...tools.length ? { tools: tools.map((tool) => ({
542
+ type: "function",
543
+ function: {
544
+ name: tool.name,
545
+ description: tool.description,
546
+ parameters: tool.inputSchema
547
+ }
548
+ })) } : {},
549
+ ...think === void 0 ? {} : { think },
550
+ options: {
551
+ num_ctx: context,
552
+ num_predict: Number(maxTokens),
553
+ ...params.temperature === void 0 ? {} : { temperature: params.temperature }
554
+ }
555
+ };
556
+ if (Buffer.byteLength(JSON.stringify(body)) > 20971520) throw new Error("Ollama request exceeds 20 MiB");
557
+ const text = JSON.stringify({
558
+ ...body,
559
+ messages: body.messages.map(({ images: _images, ...message }) => message)
560
+ });
561
+ return {
562
+ body,
563
+ inspection: support.inspect({
564
+ body,
565
+ textTokens: support.measureText(text),
566
+ tokenizer: "o200k_base-ollama-text-v1",
567
+ request,
568
+ capacity: { context },
569
+ mediaTokens: media.tokens,
570
+ outputReserve: Number(maxTokens),
571
+ outputReserveSource: "enforced",
572
+ fingerprint: {
573
+ rendering: "ollama-chat-v1",
574
+ endpoint: call.connectionFingerprint ?? root,
575
+ model: call.model,
576
+ profile: selection.profile,
577
+ settings: body.options,
578
+ think,
579
+ media: media.sent.map((file) => ({
580
+ id: file.ref.forModel?.id ?? file.ref.id,
581
+ size: file.ref.forModel?.size ?? file.ref.size
582
+ }))
583
+ }
584
+ })
585
+ };
586
+ }
587
+ return {
588
+ inspect(request) {
589
+ return build(request).inspection;
590
+ },
591
+ stream(request, signal, observer) {
592
+ const { body, inspection } = build(request);
593
+ if (inspection.inputTokens + inspection.outputReserve > body.options.num_ctx) throw support.failures.create({
594
+ code: "context-limit-exceeded",
595
+ stage: "request",
596
+ category: "request-too-large",
597
+ source: "protocol",
598
+ retryable: false
599
+ });
600
+ return support.withChatAttempts({
601
+ signal: AbortSignal.any([signal, AbortSignal.timeout(9e5)]),
602
+ maxRetries: 0,
603
+ async *attempt(attemptSignal, progress, attempt) {
604
+ await revalidateModel({
605
+ ...call,
606
+ model: call.model,
607
+ signal: attemptSignal
608
+ }, fetch, selection);
609
+ const response = support.observeResponseProgress(await daemonFetch(fetch, root, "chat", attemptSignal, body), progress);
610
+ try {
611
+ observer?.dispatched({ attempt });
612
+ } catch {}
613
+ let text = "";
614
+ let thinking = "";
615
+ const tools = /* @__PURE__ */ new Map();
616
+ const runId = randomUUID();
617
+ let done = false;
618
+ let final;
619
+ for await (const raw of readFrames(response, attemptSignal)) {
620
+ progress();
621
+ if (done) throw new Error("Ollama returned frames after its terminal response");
622
+ const frame = frameSchema.parse(raw);
623
+ if (frame.error) throw new Error("Ollama reported a stream failure");
624
+ if (frame.message?.thinking) {
625
+ thinking += frame.message.thinking;
626
+ yield {
627
+ kind: "thinking-delta",
628
+ text: frame.message.thinking
629
+ };
630
+ }
631
+ for (const tool of frame.message?.tool_calls ?? []) {
632
+ if (request.toolChoice === "none" || !request.tools?.some((definition) => definition.name === tool.function.name)) throw new Error("Ollama returned an undeclared tool call");
633
+ const key = tool.id ? `id:${tool.id}` : tool.function.index === void 0 ? `position:${tools.size}` : `index:${tool.function.index}`;
634
+ const previous = tools.get(key);
635
+ if (previous) {
636
+ if (previous.name !== tool.function.name || JSON.stringify(previous.input) !== JSON.stringify(tool.function.arguments)) throw new Error("Ollama changed an already completed tool call");
637
+ continue;
638
+ }
639
+ const id = tool.id ?? `ollama-${runId}-${tools.size}`;
640
+ if ([...tools.values()].some((value) => value.id === id)) throw new Error("Ollama returned duplicate tool identities");
641
+ tools.set(key, {
642
+ id,
643
+ name: tool.function.name,
644
+ input: tool.function.arguments
645
+ });
646
+ yield {
647
+ kind: "tool-call-delta",
648
+ id,
649
+ name: tool.function.name,
650
+ inputDelta: JSON.stringify(tool.function.arguments)
651
+ };
652
+ }
653
+ if (frame.message?.content) {
654
+ text += frame.message.content;
655
+ yield {
656
+ kind: "text-delta",
657
+ text: frame.message.content
658
+ };
659
+ }
660
+ if (!frame.done) continue;
661
+ done = true;
662
+ if (!["stop", "length"].includes(frame.done_reason ?? "")) throw new Error("Ollama returned an unsupported finish reason");
663
+ if (frame.prompt_eval_count === void 0 || frame.eval_count === void 0 || !text && !tools.size && !(thinking && frame.done_reason === "length")) throw new Error("Ollama returned incomplete usage or an empty response");
664
+ const content = [
665
+ ...thinking ? [{
666
+ type: "thinking",
667
+ text: thinking
668
+ }] : [],
669
+ ...text ? [{
670
+ type: "text",
671
+ text
672
+ }] : [],
673
+ ...[...tools.values()].map((tool) => ({
674
+ type: "tool-use",
675
+ ...tool
676
+ }))
677
+ ];
678
+ final = {
679
+ kind: "final",
680
+ finishReason: frame.done_reason === "length" ? "max-tokens" : tools.size ? "tool-calls" : "stop",
681
+ ...frame.done_reason === "length" && thinking ? { outputTruncation: {
682
+ incompleteThinking: true,
683
+ incompleteToolCalls: []
684
+ } } : {},
685
+ usage: {
686
+ inputTokens: frame.prompt_eval_count,
687
+ outputTokens: frame.eval_count,
688
+ ...frame.prompt_eval_cached_count === void 0 ? {} : { cacheReadTokens: frame.prompt_eval_cached_count }
689
+ },
690
+ message: {
691
+ role: "assistant",
692
+ content
693
+ }
694
+ };
695
+ }
696
+ if (!done || !final) throw support.interruptedFailure();
697
+ yield final;
698
+ },
699
+ classify(error) {
700
+ if (support.failures.owns(error)) return {
701
+ error,
702
+ retryable: false
703
+ };
704
+ if (error instanceof DaemonHttpError) {
705
+ const code = error.status === 429 ? "quota-exceeded" : error.status === 413 ? "request-too-large" : error.status === 401 || error.status === 403 ? "authentication-required" : error.status === 404 ? "model-unavailable" : "http-error";
706
+ return {
707
+ error: support.failures.create({
708
+ code,
709
+ stage: "transport",
710
+ category: error.status === 413 ? "request-too-large" : "provider",
711
+ source: "provider",
712
+ retryable: false,
713
+ status: error.status
714
+ }),
715
+ retryable: false
716
+ };
717
+ }
718
+ return {
719
+ error: support.failures.create({
720
+ code: error instanceof TypeError ? "stream-interrupted" : "response-invalid",
721
+ stage: error instanceof TypeError ? "transport" : "stream",
722
+ category: error instanceof TypeError ? "network" : "protocol",
723
+ source: error instanceof TypeError ? "transport" : "protocol",
724
+ retryable: false
725
+ }),
726
+ retryable: false
727
+ };
728
+ }
729
+ });
730
+ }
731
+ };
732
+ }
733
+ //#endregion
734
+ //#region src/support.ts
735
+ /** Normalizes official facts without requiring an installed Chat capability. */
736
+ function normalizeOffers(input) {
737
+ return Object.fromEntries(input.offers.map((offer) => [offer.model, {
738
+ id: offer.model,
739
+ ...offer.name === void 0 ? {} : { name: offer.name },
740
+ ...offer.reasoning === void 0 && !offer.efforts?.length ? {} : { reasoning: offer.efforts?.length ? true : offer.reasoning },
741
+ ...offer.efforts?.length ? { reasoning_options: [{
742
+ type: "effort",
743
+ values: [...offer.efforts]
744
+ }] } : {},
745
+ ...offer.temperature === void 0 ? {} : { temperature: offer.temperature },
746
+ ...offer.toolCall === void 0 ? {} : { tool_call: offer.toolCall },
747
+ ...offer.input === void 0 ? {} : { attachment: offer.input.some((one) => one !== "text") },
748
+ ...offer.input === void 0 && offer.output === void 0 ? {} : { modalities: {
749
+ ...offer.input === void 0 ? {} : { input: [...offer.input] },
750
+ ...offer.output === void 0 ? {} : { output: [...offer.output] }
751
+ } },
752
+ ...!offer.contextWindow && !offer.maxOutput ? {} : { limit: {
753
+ ...offer.contextWindow && offer.contextWindow > 0 ? { context: offer.contextWindow } : {},
754
+ ...offer.maxOutput && offer.maxOutput > 0 ? { output: offer.maxOutput } : {}
755
+ } },
756
+ ...offer.releaseDate === void 0 ? {} : { release_date: offer.releaseDate }
757
+ }]));
758
+ }
759
+ /** The owner-qualified accessor is read per operation; withdrawal never exposes an old service. */
760
+ function lazySupport(read) {
761
+ return new Proxy({}, { get(_target, key) {
762
+ if (key === "normalizeListing") return normalizeOffers;
763
+ const service = read();
764
+ if (!service) throw new Error("Chat model support is unavailable");
765
+ const value = Reflect.get(service, key);
766
+ return typeof value === "function" ? value.bind(service) : value;
767
+ } });
768
+ }
769
+ //#endregion
770
+ //#region src/protocol.ts
771
+ /** Native parameter support; per-model thinking switches require explicit show metadata. */
772
+ const facts = {
773
+ outputLimit: {
774
+ support: "optional",
775
+ ordinary: 1024
776
+ },
777
+ temperature: {
778
+ min: 0,
779
+ max: 2,
780
+ step: .05,
781
+ default: .8
782
+ },
783
+ thinkingSwitch: false,
784
+ modelThinkingSwitch: true,
785
+ limits: {
786
+ requestBytes: 20971520,
787
+ images: 50
788
+ }
789
+ };
790
+ /** Registers no-key native discovery, conversation and conservative experimental image execution. */
791
+ function ollamaProtocolType(support, fetch) {
792
+ return {
793
+ type: "ollama",
794
+ label: "Ollama API",
795
+ logo: OLLAMA_LOGO,
796
+ fields: [{
797
+ key: "baseUrl",
798
+ kind: "url",
799
+ label: "Base URL",
800
+ required: true,
801
+ default: "http://localhost:11434"
802
+ }],
803
+ userConfigurable: false,
804
+ optionsSchema: z.strictObject({}),
805
+ facts,
806
+ validateExecution,
807
+ prepareParams: (input) => support.prepareParams(input, facts),
808
+ adapters: {
809
+ chat: {
810
+ fileKinds: ["image"],
811
+ supportsModel(model, input) {
812
+ const execution = input.configuredModels.find((row) => row.id === model)?.execution;
813
+ return execution?.protocolId === "ollama" && Boolean(execution.sourceIdentity && parseChatProfile(execution.profile));
814
+ },
815
+ createPort: (call) => createOllamaPort(call, support, fetch)
816
+ },
817
+ image: {
818
+ models: (input) => input.configuredModels.filter((row) => row.execution?.protocolId === "ollama" && row.execution.profile === "image-v1:0.15.1" && Boolean(row.execution.sourceIdentity)).map((row) => imageModel(row.id)),
819
+ createPort: (call) => createImagePort(call, fetch)
820
+ }
821
+ },
822
+ listModels: (probe) => discoverModels(probe, fetch),
823
+ normalizeListing(input) {
824
+ const normalized = { ...normalizeOffers(input) };
825
+ for (const offer of input.offers) {
826
+ const current = normalized[offer.model];
827
+ if (!current) continue;
828
+ const profile = offer.execution?.map((candidate) => parseChatProfile(candidate.profile)).find(Boolean);
829
+ const options = [];
830
+ if (profile?.thinking.includes(true) && profile.thinking.includes(false)) options.push({ type: "toggle" });
831
+ const efforts = profile?.thinking.filter((value) => typeof value === "string") ?? [];
832
+ if (efforts.length) options.push({
833
+ type: "effort",
834
+ values: efforts
835
+ });
836
+ normalized[offer.model] = {
837
+ ...current,
838
+ reasoning_options: options
839
+ };
840
+ }
841
+ return normalized;
842
+ },
843
+ async testConnection(probe) {
844
+ const signal = AbortSignal.any([probe.signal, AbortSignal.timeout(3e4)]);
845
+ const root = daemonRoot(probe.config.baseUrl);
846
+ const budget = { remaining: Math.min(probe.maxResponseBytes ?? 8388608, 8388608) };
847
+ z.object({ version: z.string().min(1) }).parse(await readJson(await daemonFetch(fetch, root, "version", signal), signal, budget));
848
+ z.object({ models: z.array(z.unknown()).max(1e3) }).parse(await readJson(await daemonFetch(fetch, root, "tags", signal), signal, budget));
849
+ }
850
+ };
851
+ }
852
+ //#endregion
853
+ //#region src/index.ts
854
+ /** Protocol extensions contribute no model tools. */
855
+ const TOOL_NAMES = [];
856
+ const plugin = {
857
+ name: "@eum-agent/llm-protocol-ollama",
858
+ requires: ["network"],
859
+ optionalServices: ["model-support"],
860
+ apply(ctx) {
861
+ ctx.modelProtocols.register(ollamaProtocolType(lazySupport(() => ctx.services.find("model-support")), ctx.services.get("network").fetch));
862
+ }
863
+ };
864
+ //#endregion
865
+ export { TOOL_NAMES, plugin as default };
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@eum-agent/llm-protocol-ollama",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "displayName": {
4
+ "en-US": "Ollama Model Protocol",
5
+ "zh-CN": "模型协议 · Ollama"
6
+ },
7
+ "version": "1.1.0",
8
+ "type": "module",
9
+ "description": "Native local Ollama model protocol",
10
+ "license": "MIT",
11
+ "exports": {
12
+ ".": {
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "assets"
19
+ ],
20
+ "devDependencies": {
21
+ "@eum-agent/plugin": "1.1.0"
22
+ },
23
+ "dependencies": {
24
+ "zod": "^4.4.3"
25
+ },
26
+ "yaoguang": {
27
+ "kind": "extension",
28
+ "dependencies": {
29
+ "@eum-agent/llm-management": "1.1.0"
30
+ },
31
+ "foundationDependencies": {
32
+ "@eum-agent/infra-local": "1.0.1"
33
+ },
34
+ "description": {
35
+ "en-US": "Native local Ollama model protocol",
36
+ "zh-CN": "Ollama 本地模型原生接入"
37
+ }
38
+ },
39
+ "scripts": {
40
+ "build": "tsdown",
41
+ "test": "vitest run",
42
+ "typecheck": "tsc --noEmit"
43
+ }
6
44
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.