@warlock.js/ai-ollama 4.1.1
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/cjs/index.cjs +705 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/config.type.d.mts +80 -0
- package/esm/config.type.d.mts.map +1 -0
- package/esm/embedder.mjs +101 -0
- package/esm/embedder.mjs.map +1 -0
- package/esm/index.d.mts +3 -0
- package/esm/index.mjs +3 -0
- package/esm/known-vision-models.mjs +44 -0
- package/esm/known-vision-models.mjs.map +1 -0
- package/esm/model.mjs +251 -0
- package/esm/model.mjs.map +1 -0
- package/esm/sdk.d.mts +62 -0
- package/esm/sdk.d.mts.map +1 -0
- package/esm/sdk.mjs +78 -0
- package/esm/sdk.mjs.map +1 -0
- package/esm/utils/index.mjs +6 -0
- package/esm/utils/map-done-reason.mjs +31 -0
- package/esm/utils/map-done-reason.mjs.map +1 -0
- package/esm/utils/to-ollama-messages.mjs +87 -0
- package/esm/utils/to-ollama-messages.mjs.map +1 -0
- package/esm/utils/to-ollama-tools.mjs +41 -0
- package/esm/utils/to-ollama-tools.mjs.map +1 -0
- package/esm/utils/wrap-ollama-error.mjs +104 -0
- package/esm/utils/wrap-ollama-error.mjs.map +1 -0
- package/llms-full.txt +122 -0
- package/llms.txt +9 -0
- package/package.json +38 -0
- package/skills/README.md +9 -0
- package/skills/setup-ollama/SKILL.md +112 -0
package/esm/sdk.d.mts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { OllamaEmbedderConfig, OllamaModelConfig, OllamaSDKConfig } from "./config.type.mjs";
|
|
2
|
+
import { EmbedderContract, ModelContract, SDKAdapterContract } from "@warlock.js/ai";
|
|
3
|
+
|
|
4
|
+
//#region ../../@warlock.js/ai-ollama/src/sdk.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Ollama-backed implementation of `SDKAdapterContract`.
|
|
7
|
+
*
|
|
8
|
+
* **Role.** The package entry point for local / self-hosted models
|
|
9
|
+
* served by an Ollama daemon via the official `ollama` client. One
|
|
10
|
+
* `OllamaSDK` holds one live `Ollama` client, shared by every
|
|
11
|
+
* `ModelContract` / `EmbedderContract` it produces.
|
|
12
|
+
*
|
|
13
|
+
* **Responsibility.**
|
|
14
|
+
* - Owns: a long-lived `Ollama` client (host, headers) and its
|
|
15
|
+
* lifetime. Factory for `OllamaModel` / `OllamaEmbedder` instances
|
|
16
|
+
* sharing that client.
|
|
17
|
+
* - Does NOT own: anything per-call — those live in `OllamaModel` /
|
|
18
|
+
* `OllamaEmbedder` and the agent runtime.
|
|
19
|
+
*
|
|
20
|
+
* Modeled as a class (see §4.2 of code-style.md — "long-lived state
|
|
21
|
+
* across many calls"), fronted by FP usage like the other adapters.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const ollama = new OllamaSDK({}); // local default host
|
|
25
|
+
* const model = ollama.model({ name: "llama3.1", temperature: 0.7 });
|
|
26
|
+
* const embedder = ollama.embedder({ name: "nomic-embed-text" });
|
|
27
|
+
*/
|
|
28
|
+
declare class OllamaSDK implements SDKAdapterContract {
|
|
29
|
+
private readonly client;
|
|
30
|
+
private readonly provider;
|
|
31
|
+
private readonly pricing?;
|
|
32
|
+
constructor(config?: OllamaSDKConfig);
|
|
33
|
+
/**
|
|
34
|
+
* Build an `OllamaModel` bound to this SDK's client. Each call
|
|
35
|
+
* returns a fresh instance; all instances share the underlying
|
|
36
|
+
* `Ollama` client. The SDK's `provider` label is forwarded.
|
|
37
|
+
*
|
|
38
|
+
* Pricing resolution: per-model `config.pricing` wins; otherwise the
|
|
39
|
+
* SDK-level registry entry keyed by `config.name`; otherwise
|
|
40
|
+
* `undefined` (local Ollama is free, so usually undefined).
|
|
41
|
+
*/
|
|
42
|
+
model(config: OllamaModelConfig): ModelContract;
|
|
43
|
+
/**
|
|
44
|
+
* Rough token-count estimate. Uses the character-heuristic
|
|
45
|
+
* (`approximateTokenCount`) from the core package — good enough for
|
|
46
|
+
* budgeting / context guards, not billing (and Ollama is free
|
|
47
|
+
* anyway). The optional model id is reserved for future per-model
|
|
48
|
+
* tokenizer dispatch; currently ignored.
|
|
49
|
+
*/
|
|
50
|
+
count(text: string, _model?: string): Promise<number>;
|
|
51
|
+
/**
|
|
52
|
+
* Build an `OllamaEmbedder` bound to this SDK's client.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* const embedder = ollama.embedder({ name: "nomic-embed-text" });
|
|
56
|
+
* const { vector } = await embedder.embed("Hello world");
|
|
57
|
+
*/
|
|
58
|
+
embedder(config: OllamaEmbedderConfig): EmbedderContract;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { OllamaSDK };
|
|
62
|
+
//# sourceMappingURL=sdk.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../@warlock.js/ai-ollama/src/sdk.ts"],"mappings":";;;;;;AAuCA;;;;;;;;;;;;;;;;;;;;;cAAa,SAAA,YAAqB,kBAAA;EAAA,iBACf,MAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;cAEE,MAAA,GAAQ,eAAA;EAgCwB;;;;;;AAWY;;;EA1BxD,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,aAAA;;;;;;;;EAe5B,KAAA,CAAM,IAAA,UAAc,MAAA,YAAkB,OAAA;;;;;;;;EAW5C,QAAA,CAAS,MAAA,EAAQ,oBAAA,GAAuB,gBAAA;AAAA"}
|
package/esm/sdk.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { OllamaEmbedder } from "./embedder.mjs";
|
|
2
|
+
import { OllamaModel } from "./model.mjs";
|
|
3
|
+
import { Ollama } from "ollama";
|
|
4
|
+
import { approximateTokenCount } from "@warlock.js/ai";
|
|
5
|
+
|
|
6
|
+
//#region ../../@warlock.js/ai-ollama/src/sdk.ts
|
|
7
|
+
/**
|
|
8
|
+
* Ollama-backed implementation of `SDKAdapterContract`.
|
|
9
|
+
*
|
|
10
|
+
* **Role.** The package entry point for local / self-hosted models
|
|
11
|
+
* served by an Ollama daemon via the official `ollama` client. One
|
|
12
|
+
* `OllamaSDK` holds one live `Ollama` client, shared by every
|
|
13
|
+
* `ModelContract` / `EmbedderContract` it produces.
|
|
14
|
+
*
|
|
15
|
+
* **Responsibility.**
|
|
16
|
+
* - Owns: a long-lived `Ollama` client (host, headers) and its
|
|
17
|
+
* lifetime. Factory for `OllamaModel` / `OllamaEmbedder` instances
|
|
18
|
+
* sharing that client.
|
|
19
|
+
* - Does NOT own: anything per-call — those live in `OllamaModel` /
|
|
20
|
+
* `OllamaEmbedder` and the agent runtime.
|
|
21
|
+
*
|
|
22
|
+
* Modeled as a class (see §4.2 of code-style.md — "long-lived state
|
|
23
|
+
* across many calls"), fronted by FP usage like the other adapters.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* const ollama = new OllamaSDK({}); // local default host
|
|
27
|
+
* const model = ollama.model({ name: "llama3.1", temperature: 0.7 });
|
|
28
|
+
* const embedder = ollama.embedder({ name: "nomic-embed-text" });
|
|
29
|
+
*/
|
|
30
|
+
var OllamaSDK = class {
|
|
31
|
+
constructor(config = {}) {
|
|
32
|
+
const { provider, pricing, ...clientConfig } = config;
|
|
33
|
+
this.client = new Ollama(clientConfig);
|
|
34
|
+
this.provider = provider ?? "ollama";
|
|
35
|
+
this.pricing = pricing;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build an `OllamaModel` bound to this SDK's client. Each call
|
|
39
|
+
* returns a fresh instance; all instances share the underlying
|
|
40
|
+
* `Ollama` client. The SDK's `provider` label is forwarded.
|
|
41
|
+
*
|
|
42
|
+
* Pricing resolution: per-model `config.pricing` wins; otherwise the
|
|
43
|
+
* SDK-level registry entry keyed by `config.name`; otherwise
|
|
44
|
+
* `undefined` (local Ollama is free, so usually undefined).
|
|
45
|
+
*/
|
|
46
|
+
model(config) {
|
|
47
|
+
const resolvedPricing = config.pricing ?? this.pricing?.[config.name];
|
|
48
|
+
const resolvedConfig = resolvedPricing === config.pricing ? config : {
|
|
49
|
+
...config,
|
|
50
|
+
pricing: resolvedPricing
|
|
51
|
+
};
|
|
52
|
+
return new OllamaModel(this.client, resolvedConfig, this.provider);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Rough token-count estimate. Uses the character-heuristic
|
|
56
|
+
* (`approximateTokenCount`) from the core package — good enough for
|
|
57
|
+
* budgeting / context guards, not billing (and Ollama is free
|
|
58
|
+
* anyway). The optional model id is reserved for future per-model
|
|
59
|
+
* tokenizer dispatch; currently ignored.
|
|
60
|
+
*/
|
|
61
|
+
async count(text, _model) {
|
|
62
|
+
return approximateTokenCount(text);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build an `OllamaEmbedder` bound to this SDK's client.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* const embedder = ollama.embedder({ name: "nomic-embed-text" });
|
|
69
|
+
* const { vector } = await embedder.embed("Hello world");
|
|
70
|
+
*/
|
|
71
|
+
embedder(config) {
|
|
72
|
+
return new OllamaEmbedder(this.client, config, this.provider);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { OllamaSDK };
|
|
78
|
+
//# sourceMappingURL=sdk.mjs.map
|
package/esm/sdk.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk.mjs","names":[],"sources":["../../../../../@warlock.js/ai-ollama/src/sdk.ts"],"sourcesContent":["import { Ollama } from \"ollama\";\nimport type {\n EmbedderContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type {\n OllamaEmbedderConfig,\n OllamaModelConfig,\n OllamaSDKConfig,\n} from \"./config.type\";\nimport { OllamaEmbedder } from \"./embedder\";\nimport { OllamaModel } from \"./model\";\n\n/**\n * Ollama-backed implementation of `SDKAdapterContract`.\n *\n * **Role.** The package entry point for local / self-hosted models\n * served by an Ollama daemon via the official `ollama` client. One\n * `OllamaSDK` holds one live `Ollama` client, shared by every\n * `ModelContract` / `EmbedderContract` it produces.\n *\n * **Responsibility.**\n * - Owns: a long-lived `Ollama` client (host, headers) and its\n * lifetime. Factory for `OllamaModel` / `OllamaEmbedder` instances\n * sharing that client.\n * - Does NOT own: anything per-call — those live in `OllamaModel` /\n * `OllamaEmbedder` and the agent runtime.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across many calls\"), fronted by FP usage like the other adapters.\n *\n * @example\n * const ollama = new OllamaSDK({}); // local default host\n * const model = ollama.model({ name: \"llama3.1\", temperature: 0.7 });\n * const embedder = ollama.embedder({ name: \"nomic-embed-text\" });\n */\nexport class OllamaSDK implements SDKAdapterContract {\n private readonly client: Ollama;\n private readonly provider: string;\n private readonly pricing?: Record<string, ModelPricing>;\n\n public constructor(config: OllamaSDKConfig = {}) {\n const { provider, pricing, ...clientConfig } = config;\n\n this.client = new Ollama(clientConfig);\n this.provider = provider ?? \"ollama\";\n this.pricing = pricing;\n }\n\n /**\n * Build an `OllamaModel` bound to this SDK's client. Each call\n * returns a fresh instance; all instances share the underlying\n * `Ollama` client. The SDK's `provider` label is forwarded.\n *\n * Pricing resolution: per-model `config.pricing` wins; otherwise the\n * SDK-level registry entry keyed by `config.name`; otherwise\n * `undefined` (local Ollama is free, so usually undefined).\n */\n public model(config: OllamaModelConfig): ModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: OllamaModelConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new OllamaModel(this.client, resolvedConfig, this.provider);\n }\n\n /**\n * Rough token-count estimate. Uses the character-heuristic\n * (`approximateTokenCount`) from the core package — good enough for\n * budgeting / context guards, not billing (and Ollama is free\n * anyway). The optional model id is reserved for future per-model\n * tokenizer dispatch; currently ignored.\n */\n public async count(text: string, _model?: string): Promise<number> {\n return approximateTokenCount(text);\n }\n\n /**\n * Build an `OllamaEmbedder` bound to this SDK's client.\n *\n * @example\n * const embedder = ollama.embedder({ name: \"nomic-embed-text\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n */\n public embedder(config: OllamaEmbedderConfig): EmbedderContract {\n return new OllamaEmbedder(this.client, config, this.provider);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,YAAb,MAAqD;CAKnD,AAAO,YAAY,SAA0B,CAAC,GAAG;EAC/C,MAAM,EAAE,UAAU,SAAS,GAAG,iBAAiB;EAE/C,KAAK,SAAS,IAAI,OAAO,YAAY;EACrC,KAAK,WAAW,YAAY;EAC5B,KAAK,UAAU;CACjB;;;;;;;;;;CAWA,AAAO,MAAM,QAA0C;EACrD,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,OAAO,IAAI,YAAY,KAAK,QAAQ,gBAAgB,KAAK,QAAQ;CACnE;;;;;;;;CASA,MAAa,MAAM,MAAc,QAAkC;EACjE,OAAO,sBAAsB,IAAI;CACnC;;;;;;;;CASA,AAAO,SAAS,QAAgD;EAC9D,OAAO,IAAI,eAAe,KAAK,QAAQ,QAAQ,KAAK,QAAQ;CAC9D;AACF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region ../../@warlock.js/ai-ollama/src/utils/map-done-reason.ts
|
|
2
|
+
const doneReasonMap = {
|
|
3
|
+
stop: "stop",
|
|
4
|
+
length: "length"
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Map Ollama's `done_reason` to the normalized `FinishReason` union.
|
|
8
|
+
*
|
|
9
|
+
* `stop` is the natural terminal; `length` means the `num_predict`
|
|
10
|
+
* cap was hit. Anything else — `load` (model load only, no
|
|
11
|
+
* generation), an empty string, or any future value — falls through
|
|
12
|
+
* to `"error"`.
|
|
13
|
+
*
|
|
14
|
+
* Note: Ollama has no tool-use done reason — it sets `done_reason:
|
|
15
|
+
* "stop"` and populates `message.tool_calls`. `OllamaModel` derives
|
|
16
|
+
* `"tool_calls"` from tool-call presence; this map stays purely about
|
|
17
|
+
* the raw signal.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* mapDoneReason("stop"); // "stop"
|
|
21
|
+
* mapDoneReason("length"); // "length"
|
|
22
|
+
* mapDoneReason("load"); // "error"
|
|
23
|
+
* mapDoneReason(undefined); // "error"
|
|
24
|
+
*/
|
|
25
|
+
function mapDoneReason(raw) {
|
|
26
|
+
return doneReasonMap[raw ?? ""] ?? "error";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { mapDoneReason };
|
|
31
|
+
//# sourceMappingURL=map-done-reason.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map-done-reason.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-ollama/src/utils/map-done-reason.ts"],"sourcesContent":["import type { FinishReason } from \"@warlock.js/ai\";\n\nconst doneReasonMap: Record<string, FinishReason> = {\n stop: \"stop\",\n length: \"length\",\n};\n\n/**\n * Map Ollama's `done_reason` to the normalized `FinishReason` union.\n *\n * `stop` is the natural terminal; `length` means the `num_predict`\n * cap was hit. Anything else — `load` (model load only, no\n * generation), an empty string, or any future value — falls through\n * to `\"error\"`.\n *\n * Note: Ollama has no tool-use done reason — it sets `done_reason:\n * \"stop\"` and populates `message.tool_calls`. `OllamaModel` derives\n * `\"tool_calls\"` from tool-call presence; this map stays purely about\n * the raw signal.\n *\n * @example\n * mapDoneReason(\"stop\"); // \"stop\"\n * mapDoneReason(\"length\"); // \"length\"\n * mapDoneReason(\"load\"); // \"error\"\n * mapDoneReason(undefined); // \"error\"\n */\nexport function mapDoneReason(raw: string | null | undefined): FinishReason {\n return doneReasonMap[raw ?? \"\"] ?? \"error\";\n}\n"],"mappings":";AAEA,MAAM,gBAA8C;CAClD,MAAM;CACN,QAAQ;AACV;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,cAAc,KAA8C;CAC1E,OAAO,cAAc,OAAO,OAAO;AACrC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { InvalidRequestError } from "@warlock.js/ai";
|
|
2
|
+
|
|
3
|
+
//#region ../../@warlock.js/ai-ollama/src/utils/to-ollama-messages.ts
|
|
4
|
+
/**
|
|
5
|
+
* Convert vendor-neutral `Message[]` into the Ollama chat message
|
|
6
|
+
* shape.
|
|
7
|
+
*
|
|
8
|
+
* Unlike Anthropic / Gemini / Bedrock, Ollama keeps a first-class
|
|
9
|
+
* `system` role inside `messages`, so there is no system-prompt
|
|
10
|
+
* hoisting — roles pass straight through. The Ollama specifics this
|
|
11
|
+
* absorbs:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Tool calls.** An assistant message with `toolCalls` becomes an
|
|
14
|
+
* `assistant` message whose `tool_calls` is the Ollama
|
|
15
|
+
* `{ function: { name, arguments } }` shape (Ollama has no tool-call
|
|
16
|
+
* id — see `OllamaModel`/decisions for the synthesized-id note).
|
|
17
|
+
* 2. **Tool results.** A neutral `tool` message becomes a `tool`
|
|
18
|
+
* message with `tool_name` set from `toolCallId` (Ollama matches a
|
|
19
|
+
* result to its call by tool name).
|
|
20
|
+
* 3. **Images.** Multipart user content collapses to a single
|
|
21
|
+
* `content` string plus an `images` array of base64 strings.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const messages = toOllamaMessages([
|
|
25
|
+
* { role: "system", content: "Be concise." },
|
|
26
|
+
* { role: "user", content: "Hi" },
|
|
27
|
+
* ]);
|
|
28
|
+
*/
|
|
29
|
+
function toOllamaMessages(messages) {
|
|
30
|
+
return messages.map((message) => {
|
|
31
|
+
if (message.role === "tool") return {
|
|
32
|
+
role: "tool",
|
|
33
|
+
content: stringifyContent(message.content),
|
|
34
|
+
tool_name: message.toolCallId ?? ""
|
|
35
|
+
};
|
|
36
|
+
if (message.role === "assistant" && message.toolCalls && message.toolCalls.length > 0) return {
|
|
37
|
+
role: "assistant",
|
|
38
|
+
content: stringifyContent(message.content),
|
|
39
|
+
tool_calls: message.toolCalls.map((toolCall) => ({ function: {
|
|
40
|
+
name: toolCall.name,
|
|
41
|
+
arguments: toolCall.input ?? {}
|
|
42
|
+
} }))
|
|
43
|
+
};
|
|
44
|
+
if (message.role === "user" && Array.isArray(message.content)) return toMultipartMessage(message.content);
|
|
45
|
+
return {
|
|
46
|
+
role: message.role,
|
|
47
|
+
content: stringifyContent(message.content)
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Collapse a `ContentPart[]` user message into Ollama's
|
|
53
|
+
* single-string-content + base64-`images` shape. Ollama cannot fetch
|
|
54
|
+
* remote URLs, so a `{ url }` image surfaces a typed
|
|
55
|
+
* `InvalidRequestError` upfront (consistent with the Bedrock/Gemini
|
|
56
|
+
* adapters). The agent has already resolved attachments — nothing is
|
|
57
|
+
* fetched here.
|
|
58
|
+
*/
|
|
59
|
+
function toMultipartMessage(parts) {
|
|
60
|
+
const textChunks = [];
|
|
61
|
+
const images = [];
|
|
62
|
+
for (const part of parts) {
|
|
63
|
+
if (part.type === "text") {
|
|
64
|
+
textChunks.push(part.text);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if ("url" in part.source) throw new InvalidRequestError("Ollama does not fetch remote-URL images; supply base64 image bytes instead.");
|
|
68
|
+
images.push(part.source.base64);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
role: "user",
|
|
72
|
+
content: textChunks.join(""),
|
|
73
|
+
...images.length > 0 ? { images } : {}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Multipart content on a non-user role collapses to concatenated text;
|
|
78
|
+
* plain strings pass through unchanged.
|
|
79
|
+
*/
|
|
80
|
+
function stringifyContent(content) {
|
|
81
|
+
if (typeof content === "string") return content;
|
|
82
|
+
return content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
export { toOllamaMessages };
|
|
87
|
+
//# sourceMappingURL=to-ollama-messages.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-ollama-messages.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-ollama/src/utils/to-ollama-messages.ts"],"sourcesContent":["import { InvalidRequestError, type ContentPart, type Message } from \"@warlock.js/ai\";\nimport type { Message as OllamaMessage } from \"ollama\";\n\n/**\n * Convert vendor-neutral `Message[]` into the Ollama chat message\n * shape.\n *\n * Unlike Anthropic / Gemini / Bedrock, Ollama keeps a first-class\n * `system` role inside `messages`, so there is no system-prompt\n * hoisting — roles pass straight through. The Ollama specifics this\n * absorbs:\n *\n * 1. **Tool calls.** An assistant message with `toolCalls` becomes an\n * `assistant` message whose `tool_calls` is the Ollama\n * `{ function: { name, arguments } }` shape (Ollama has no tool-call\n * id — see `OllamaModel`/decisions for the synthesized-id note).\n * 2. **Tool results.** A neutral `tool` message becomes a `tool`\n * message with `tool_name` set from `toolCallId` (Ollama matches a\n * result to its call by tool name).\n * 3. **Images.** Multipart user content collapses to a single\n * `content` string plus an `images` array of base64 strings.\n *\n * @example\n * const messages = toOllamaMessages([\n * { role: \"system\", content: \"Be concise.\" },\n * { role: \"user\", content: \"Hi\" },\n * ]);\n */\nexport function toOllamaMessages(messages: Message[]): OllamaMessage[] {\n return messages.map((message): OllamaMessage => {\n if (message.role === \"tool\") {\n return {\n role: \"tool\",\n content: stringifyContent(message.content),\n tool_name: message.toolCallId ?? \"\",\n };\n }\n\n if (message.role === \"assistant\" && message.toolCalls && message.toolCalls.length > 0) {\n return {\n role: \"assistant\",\n content: stringifyContent(message.content),\n tool_calls: message.toolCalls.map((toolCall) => ({\n function: {\n name: toolCall.name,\n arguments: (toolCall.input ?? {}) as Record<string, unknown>,\n },\n })),\n };\n }\n\n if (message.role === \"user\" && Array.isArray(message.content)) {\n return toMultipartMessage(message.content);\n }\n\n return { role: message.role, content: stringifyContent(message.content) };\n });\n}\n\n/**\n * Collapse a `ContentPart[]` user message into Ollama's\n * single-string-content + base64-`images` shape. Ollama cannot fetch\n * remote URLs, so a `{ url }` image surfaces a typed\n * `InvalidRequestError` upfront (consistent with the Bedrock/Gemini\n * adapters). The agent has already resolved attachments — nothing is\n * fetched here.\n */\nfunction toMultipartMessage(parts: ContentPart[]): OllamaMessage {\n const textChunks: string[] = [];\n const images: string[] = [];\n\n for (const part of parts) {\n if (part.type === \"text\") {\n textChunks.push(part.text);\n\n continue;\n }\n\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"Ollama does not fetch remote-URL images; supply base64 image bytes instead.\",\n );\n }\n\n images.push(part.source.base64);\n }\n\n return {\n role: \"user\",\n content: textChunks.join(\"\"),\n ...(images.length > 0 ? { images } : {}),\n };\n}\n\n/**\n * Multipart content on a non-user role collapses to concatenated text;\n * plain strings pass through unchanged.\n */\nfunction stringifyContent(content: string | ContentPart[]): string {\n if (typeof content === \"string\") {\n return content;\n }\n\n return content\n .filter((part): part is { type: \"text\"; text: string } => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,iBAAiB,UAAsC;CACrE,OAAO,SAAS,KAAK,YAA2B;EAC9C,IAAI,QAAQ,SAAS,QACnB,OAAO;GACL,MAAM;GACN,SAAS,iBAAiB,QAAQ,OAAO;GACzC,WAAW,QAAQ,cAAc;EACnC;EAGF,IAAI,QAAQ,SAAS,eAAe,QAAQ,aAAa,QAAQ,UAAU,SAAS,GAClF,OAAO;GACL,MAAM;GACN,SAAS,iBAAiB,QAAQ,OAAO;GACzC,YAAY,QAAQ,UAAU,KAAK,cAAc,EAC/C,UAAU;IACR,MAAM,SAAS;IACf,WAAY,SAAS,SAAS,CAAC;GACjC,EACF,EAAE;EACJ;EAGF,IAAI,QAAQ,SAAS,UAAU,MAAM,QAAQ,QAAQ,OAAO,GAC1D,OAAO,mBAAmB,QAAQ,OAAO;EAG3C,OAAO;GAAE,MAAM,QAAQ;GAAM,SAAS,iBAAiB,QAAQ,OAAO;EAAE;CAC1E,CAAC;AACH;;;;;;;;;AAUA,SAAS,mBAAmB,OAAqC;CAC/D,MAAM,aAAuB,CAAC;CAC9B,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,KAAK,SAAS,QAAQ;GACxB,WAAW,KAAK,KAAK,IAAI;GAEzB;EACF;EAEA,IAAI,SAAS,KAAK,QAChB,MAAM,IAAI,oBACR,6EACF;EAGF,OAAO,KAAK,KAAK,OAAO,MAAM;CAChC;CAEA,OAAO;EACL,MAAM;EACN,SAAS,WAAW,KAAK,EAAE;EAC3B,GAAI,OAAO,SAAS,IAAI,EAAE,OAAO,IAAI,CAAC;CACxC;AACF;;;;;AAMA,SAAS,iBAAiB,SAAyC;CACjE,IAAI,OAAO,YAAY,UACrB,OAAO;CAGT,OAAO,QACJ,QAAQ,SAAiD,KAAK,SAAS,MAAM,EAC7E,KAAK,SAAS,KAAK,IAAI,EACvB,KAAK,EAAE;AACZ"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { extractJsonSchema } from "@warlock.js/ai";
|
|
2
|
+
|
|
3
|
+
//#region ../../@warlock.js/ai-ollama/src/utils/to-ollama-tools.ts
|
|
4
|
+
/**
|
|
5
|
+
* Convert vendor-neutral `ToolConfig[]` into Ollama's `tools` array.
|
|
6
|
+
* Each tool becomes a `{ type: "function", function: { name,
|
|
7
|
+
* description, parameters } }` entry. Non-object extractions degrade
|
|
8
|
+
* to a parameterless object so registration never fails.
|
|
9
|
+
*
|
|
10
|
+
* Returns `undefined` when there are no tools so the caller can omit
|
|
11
|
+
* `tools` from the request.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const tools = toOllamaTools([weatherTool]);
|
|
15
|
+
* await ollama.chat({ model, messages, tools });
|
|
16
|
+
*/
|
|
17
|
+
function toOllamaTools(tools) {
|
|
18
|
+
if (!tools || tools.length === 0) return;
|
|
19
|
+
return tools.map((tool) => ({
|
|
20
|
+
type: "function",
|
|
21
|
+
function: {
|
|
22
|
+
name: tool.name,
|
|
23
|
+
description: tool.description,
|
|
24
|
+
parameters: toParameters(tool.input)
|
|
25
|
+
}
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a tool's input schema to a JSON-Schema object. Ollama wants
|
|
30
|
+
* an object root for function parameters; anything else (or a failed
|
|
31
|
+
* extraction) degrades to a parameterless object.
|
|
32
|
+
*/
|
|
33
|
+
function toParameters(input) {
|
|
34
|
+
const schema = extractJsonSchema(input);
|
|
35
|
+
if (schema && schema.type === "object") return schema;
|
|
36
|
+
return { type: "object" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { toOllamaTools };
|
|
41
|
+
//# sourceMappingURL=to-ollama-tools.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-ollama-tools.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-ollama/src/utils/to-ollama-tools.ts"],"sourcesContent":["import { extractJsonSchema, type ToolConfig } from \"@warlock.js/ai\";\nimport type { Tool } from \"ollama\";\n\n/**\n * Convert vendor-neutral `ToolConfig[]` into Ollama's `tools` array.\n * Each tool becomes a `{ type: \"function\", function: { name,\n * description, parameters } }` entry. Non-object extractions degrade\n * to a parameterless object so registration never fails.\n *\n * Returns `undefined` when there are no tools so the caller can omit\n * `tools` from the request.\n *\n * @example\n * const tools = toOllamaTools([weatherTool]);\n * await ollama.chat({ model, messages, tools });\n */\nexport function toOllamaTools(\n tools: ToolConfig<unknown, unknown>[] | undefined,\n): Tool[] | undefined {\n if (!tools || tools.length === 0) {\n return undefined;\n }\n\n return tools.map((tool) => ({\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description,\n parameters: toParameters(tool.input),\n },\n }));\n}\n\n/**\n * Resolve a tool's input schema to a JSON-Schema object. Ollama wants\n * an object root for function parameters; anything else (or a failed\n * extraction) degrades to a parameterless object.\n */\nfunction toParameters(input: ToolConfig<unknown, unknown>[\"input\"]): Tool[\"function\"][\"parameters\"] {\n const schema = extractJsonSchema(input);\n\n if (schema && schema.type === \"object\") {\n return schema as Tool[\"function\"][\"parameters\"];\n }\n\n return { type: \"object\" } as Tool[\"function\"][\"parameters\"];\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAAgB,cACd,OACoB;CACpB,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;CAGF,OAAO,MAAM,KAAK,UAAU;EAC1B,MAAM;EACN,UAAU;GACR,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,YAAY,aAAa,KAAK,KAAK;EACrC;CACF,EAAE;AACJ;;;;;;AAOA,SAAS,aAAa,OAA8E;CAClG,MAAM,SAAS,kBAAkB,KAAK;CAEtC,IAAI,UAAU,OAAO,SAAS,UAC5B,OAAO;CAGT,OAAO,EAAE,MAAM,SAAS;AAC1B"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { AIError, ContextLengthExceededError, InvalidRequestError, ProviderAuthError, ProviderError, ProviderRateLimitError, ProviderTimeoutError } from "@warlock.js/ai";
|
|
2
|
+
|
|
3
|
+
//#region ../../@warlock.js/ai-ollama/src/utils/wrap-ollama-error.ts
|
|
4
|
+
/**
|
|
5
|
+
* Wrap any thrown value caught inside the Ollama adapter into the
|
|
6
|
+
* appropriate `@warlock.js/ai` `AIError` subclass.
|
|
7
|
+
*
|
|
8
|
+
* **Dispatch strategy.** HTTP faults carry `status_code`; the local
|
|
9
|
+
* daemon being down surfaces as a connection error (`ECONNREFUSED` /
|
|
10
|
+
* "fetch failed") — mapped to `ProviderError` since it's an
|
|
11
|
+
* operational "is Ollama running?" condition, not a request defect.
|
|
12
|
+
* `400` with context-length phrasing maps to
|
|
13
|
+
* `ContextLengthExceededError`.
|
|
14
|
+
*
|
|
15
|
+
* `AIError` instances pass through unchanged so `catch/throw wrap(e)`
|
|
16
|
+
* pipelines never double-wrap.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* try {
|
|
20
|
+
* return await this.client.chat({ ... });
|
|
21
|
+
* } catch (thrown) {
|
|
22
|
+
* throw wrapOllamaError(thrown);
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
function wrapOllamaError(thrown) {
|
|
26
|
+
if (thrown instanceof AIError) return thrown;
|
|
27
|
+
const shape = toShape(thrown);
|
|
28
|
+
const context = buildContext(shape);
|
|
29
|
+
const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));
|
|
30
|
+
if (isTimeout(shape)) return new ProviderTimeoutError(message, {
|
|
31
|
+
cause: thrown,
|
|
32
|
+
context
|
|
33
|
+
});
|
|
34
|
+
if (isConnectionRefused(shape, message)) return new ProviderError(message, {
|
|
35
|
+
cause: thrown,
|
|
36
|
+
context
|
|
37
|
+
});
|
|
38
|
+
if (shape.statusCode === 401 || shape.statusCode === 403) return new ProviderAuthError(message, {
|
|
39
|
+
cause: thrown,
|
|
40
|
+
context
|
|
41
|
+
});
|
|
42
|
+
if (shape.statusCode === 429) return new ProviderRateLimitError(message, {
|
|
43
|
+
cause: thrown,
|
|
44
|
+
context
|
|
45
|
+
});
|
|
46
|
+
if (isClientStatus(shape.statusCode)) {
|
|
47
|
+
if (/context length|too long|exceeds|maximum context/i.test(message)) return new ContextLengthExceededError(message, {
|
|
48
|
+
cause: thrown,
|
|
49
|
+
context
|
|
50
|
+
});
|
|
51
|
+
return new InvalidRequestError(message, {
|
|
52
|
+
cause: thrown,
|
|
53
|
+
context
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return new ProviderError(message, {
|
|
57
|
+
cause: thrown,
|
|
58
|
+
context
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read the raw error shape. `ResponseError` exposes `status_code`;
|
|
63
|
+
* fetch-layer errors carry a `cause` whose `code` is the OS-level
|
|
64
|
+
* socket error.
|
|
65
|
+
*/
|
|
66
|
+
function toShape(thrown) {
|
|
67
|
+
if (typeof thrown !== "object" || thrown === null) return {};
|
|
68
|
+
const raw = thrown;
|
|
69
|
+
const cause = raw.cause;
|
|
70
|
+
return {
|
|
71
|
+
name: typeof raw.name === "string" ? raw.name : void 0,
|
|
72
|
+
message: typeof raw.message === "string" ? raw.message : void 0,
|
|
73
|
+
statusCode: typeof raw.status_code === "number" ? raw.status_code : void 0,
|
|
74
|
+
code: typeof raw.code === "string" ? raw.code : cause && typeof cause.code === "string" ? cause.code : void 0
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Transport-level timeout signals. */
|
|
78
|
+
function isTimeout(shape) {
|
|
79
|
+
if (shape.name === "AbortError" || shape.name === "TimeoutError") return true;
|
|
80
|
+
return shape.code === "ETIMEDOUT" || shape.code === "ECONNABORTED";
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The Ollama daemon not being reachable (most common local failure):
|
|
84
|
+
* connection refused at the socket layer, or the `fetch failed`
|
|
85
|
+
* TypeError the client surfaces when the host is down.
|
|
86
|
+
*/
|
|
87
|
+
function isConnectionRefused(shape, message) {
|
|
88
|
+
return shape.code === "ECONNREFUSED" || /fetch failed|econnrefused/i.test(message);
|
|
89
|
+
}
|
|
90
|
+
/** True for HTTP 4xx — a client-side request problem, not a server fault. */
|
|
91
|
+
function isClientStatus(status) {
|
|
92
|
+
return typeof status === "number" && status >= 400 && status < 500;
|
|
93
|
+
}
|
|
94
|
+
/** Attach the diagnostic fields to `error.context`. */
|
|
95
|
+
function buildContext(shape) {
|
|
96
|
+
const context = {};
|
|
97
|
+
if (shape.statusCode !== void 0) context.status = shape.statusCode;
|
|
98
|
+
if (shape.code) context.code = shape.code;
|
|
99
|
+
return context;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
export { wrapOllamaError };
|
|
104
|
+
//# sourceMappingURL=wrap-ollama-error.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrap-ollama-error.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-ollama/src/utils/wrap-ollama-error.ts"],"sourcesContent":["import {\n AIError,\n ContextLengthExceededError,\n InvalidRequestError,\n ProviderAuthError,\n ProviderError,\n ProviderRateLimitError,\n ProviderTimeoutError,\n} from \"@warlock.js/ai\";\n\n/**\n * Raw-error fields the wrapper reads off an Ollama client error. The\n * `ollama` client throws a `ResponseError` (`name: \"ResponseError\"`,\n * numeric `status_code`, message = the server's `error` text) for HTTP\n * faults; transport failures surface as a `fetch`-layer `TypeError`\n * with an `ECONNREFUSED` / `ETIMEDOUT` cause. We duck-type both —\n * `ResponseError` is internal to the package and not exported.\n */\ntype OllamaErrorShape = {\n name?: string;\n message?: string;\n statusCode?: number;\n code?: string;\n};\n\n/**\n * Wrap any thrown value caught inside the Ollama adapter into the\n * appropriate `@warlock.js/ai` `AIError` subclass.\n *\n * **Dispatch strategy.** HTTP faults carry `status_code`; the local\n * daemon being down surfaces as a connection error (`ECONNREFUSED` /\n * \"fetch failed\") — mapped to `ProviderError` since it's an\n * operational \"is Ollama running?\" condition, not a request defect.\n * `400` with context-length phrasing maps to\n * `ContextLengthExceededError`.\n *\n * `AIError` instances pass through unchanged so `catch/throw wrap(e)`\n * pipelines never double-wrap.\n *\n * @example\n * try {\n * return await this.client.chat({ ... });\n * } catch (thrown) {\n * throw wrapOllamaError(thrown);\n * }\n */\nexport function wrapOllamaError(thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n const shape = toShape(thrown);\n const context = buildContext(shape);\n const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));\n\n if (isTimeout(shape)) {\n return new ProviderTimeoutError(message, { cause: thrown, context });\n }\n\n if (isConnectionRefused(shape, message)) {\n return new ProviderError(message, { cause: thrown, context });\n }\n\n if (shape.statusCode === 401 || shape.statusCode === 403) {\n return new ProviderAuthError(message, { cause: thrown, context });\n }\n\n if (shape.statusCode === 429) {\n return new ProviderRateLimitError(message, { cause: thrown, context });\n }\n\n if (isClientStatus(shape.statusCode)) {\n if (/context length|too long|exceeds|maximum context/i.test(message)) {\n return new ContextLengthExceededError(message, { cause: thrown, context });\n }\n\n return new InvalidRequestError(message, { cause: thrown, context });\n }\n\n return new ProviderError(message, { cause: thrown, context });\n}\n\n/**\n * Read the raw error shape. `ResponseError` exposes `status_code`;\n * fetch-layer errors carry a `cause` whose `code` is the OS-level\n * socket error.\n */\nfunction toShape(thrown: unknown): OllamaErrorShape {\n if (typeof thrown !== \"object\" || thrown === null) {\n return {};\n }\n\n const raw = thrown as Record<string, unknown>;\n const cause = raw.cause as Record<string, unknown> | undefined;\n\n return {\n name: typeof raw.name === \"string\" ? raw.name : undefined,\n message: typeof raw.message === \"string\" ? raw.message : undefined,\n statusCode: typeof raw.status_code === \"number\" ? raw.status_code : undefined,\n code:\n typeof raw.code === \"string\"\n ? raw.code\n : cause && typeof cause.code === \"string\"\n ? cause.code\n : undefined,\n };\n}\n\n/** Transport-level timeout signals. */\nfunction isTimeout(shape: OllamaErrorShape): boolean {\n if (shape.name === \"AbortError\" || shape.name === \"TimeoutError\") {\n return true;\n }\n\n return shape.code === \"ETIMEDOUT\" || shape.code === \"ECONNABORTED\";\n}\n\n/**\n * The Ollama daemon not being reachable (most common local failure):\n * connection refused at the socket layer, or the `fetch failed`\n * TypeError the client surfaces when the host is down.\n */\nfunction isConnectionRefused(shape: OllamaErrorShape, message: string): boolean {\n return shape.code === \"ECONNREFUSED\" || /fetch failed|econnrefused/i.test(message);\n}\n\n/** True for HTTP 4xx — a client-side request problem, not a server fault. */\nfunction isClientStatus(status: number | undefined): boolean {\n return typeof status === \"number\" && status >= 400 && status < 500;\n}\n\n/** Attach the diagnostic fields to `error.context`. */\nfunction buildContext(shape: OllamaErrorShape): Record<string, unknown> {\n const context: Record<string, unknown> = {};\n\n if (shape.statusCode !== undefined) {\n context.status = shape.statusCode;\n }\n\n if (shape.code) {\n context.code = shape.code;\n }\n\n return context;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA8CA,SAAgB,gBAAgB,QAA0B;CACxD,IAAI,kBAAkB,SACpB,OAAO;CAGT,MAAM,QAAQ,QAAQ,MAAM;CAC5B,MAAM,UAAU,aAAa,KAAK;CAClC,MAAM,UAAU,MAAM,YAAY,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM;CAE1F,IAAI,UAAU,KAAK,GACjB,OAAO,IAAI,qBAAqB,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAGrE,IAAI,oBAAoB,OAAO,OAAO,GACpC,OAAO,IAAI,cAAc,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAG9D,IAAI,MAAM,eAAe,OAAO,MAAM,eAAe,KACnD,OAAO,IAAI,kBAAkB,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAGlE,IAAI,MAAM,eAAe,KACvB,OAAO,IAAI,uBAAuB,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAGvE,IAAI,eAAe,MAAM,UAAU,GAAG;EACpC,IAAI,mDAAmD,KAAK,OAAO,GACjE,OAAO,IAAI,2BAA2B,SAAS;GAAE,OAAO;GAAQ;EAAQ,CAAC;EAG3E,OAAO,IAAI,oBAAoB,SAAS;GAAE,OAAO;GAAQ;EAAQ,CAAC;CACpE;CAEA,OAAO,IAAI,cAAc,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;AAC9D;;;;;;AAOA,SAAS,QAAQ,QAAmC;CAClD,IAAI,OAAO,WAAW,YAAY,WAAW,MAC3C,OAAO,CAAC;CAGV,MAAM,MAAM;CACZ,MAAM,QAAQ,IAAI;CAElB,OAAO;EACL,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;EAChD,SAAS,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU;EACzD,YAAY,OAAO,IAAI,gBAAgB,WAAW,IAAI,cAAc;EACpE,MACE,OAAO,IAAI,SAAS,WAChB,IAAI,OACJ,SAAS,OAAO,MAAM,SAAS,WAC7B,MAAM,OACN;CACV;AACF;;AAGA,SAAS,UAAU,OAAkC;CACnD,IAAI,MAAM,SAAS,gBAAgB,MAAM,SAAS,gBAChD,OAAO;CAGT,OAAO,MAAM,SAAS,eAAe,MAAM,SAAS;AACtD;;;;;;AAOA,SAAS,oBAAoB,OAAyB,SAA0B;CAC9E,OAAO,MAAM,SAAS,kBAAkB,6BAA6B,KAAK,OAAO;AACnF;;AAGA,SAAS,eAAe,QAAqC;CAC3D,OAAO,OAAO,WAAW,YAAY,UAAU,OAAO,SAAS;AACjE;;AAGA,SAAS,aAAa,OAAkD;CACtE,MAAM,UAAmC,CAAC;CAE1C,IAAI,MAAM,eAAe,QACvB,QAAQ,SAAS,MAAM;CAGzB,IAAI,MAAM,MACR,QAAQ,OAAO,MAAM;CAGvB,OAAO;AACT"}
|
package/llms-full.txt
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Warlock AI Ollama — full skills
|
|
2
|
+
|
|
3
|
+
> Package: `@warlock.js/ai-ollama`
|
|
4
|
+
|
|
5
|
+
> Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/ai-ollama/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
|
|
6
|
+
|
|
7
|
+
## setup-ollama `@warlock.js/ai-ollama/setup-ollama/SKILL.md`
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
name: setup-ollama
|
|
11
|
+
description: 'Wire @warlock.js/ai-ollama — new OllamaSDK({host?, headers?}) for local / self-hosted Ollama via the official ollama client (not OpenAI-compat). chat + embed, daemon-down error handling. Triggers: `OllamaSDK`, `ollama.model`, `ollama.embedder`, `embedder.embedMany`, `ollama.count`, `host`, `headers`; "use ollama with warlock", "run llama3 locally", "self-hosted llama"; typical import `import { OllamaSDK } from "@warlock.js/ai-ollama"`. Skip: agent loop — `@warlock.js/ai/run-ai-agent/SKILL.md`; provider choice — `@warlock.js/ai/pick-ai-provider/SKILL.md`; embeddings core — `@warlock.js/ai/embed-text/SKILL.md`; siblings `@warlock.js/ai-openai`, `@warlock.js/ai-anthropic`, `@warlock.js/ai-google`; raw `ollama` npm, Vercel `@ai-sdk/ollama`; OpenAI-compat gateway via `@warlock.js/ai-openai` `baseURL`.'
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# `@warlock.js/ai-ollama`
|
|
15
|
+
|
|
16
|
+
Provider adapter that turns a local (or self-hosted) Ollama server into a vendor-neutral `ModelContract`, plus an Ollama embedder. Uses the **official `ollama` npm package** (not OpenAI-compat). Mirrors the openai / anthropic / bedrock / google adapters.
|
|
17
|
+
|
|
18
|
+
## Construction
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { OllamaSDK } from "@warlock.js/ai-ollama";
|
|
22
|
+
|
|
23
|
+
const ollama = new OllamaSDK(); // local default host
|
|
24
|
+
const remote = new OllamaSDK({ host: "http://gpu-box.internal:11434" });
|
|
25
|
+
const gated = new OllamaSDK({
|
|
26
|
+
host: "https://ollama.internal",
|
|
27
|
+
headers: { Authorization: `Bearer ${process.env.OLLAMA_TOKEN}` },
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`OllamaSDK` is a class with a long-lived `Ollama` client. Config is `Partial<Config>` (host defaults to `http://127.0.0.1:11434`) + `provider` (default `"ollama"`) + optional `pricing` (local is free; kept for parity/chargeback).
|
|
32
|
+
|
|
33
|
+
## Producing a model
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
ollama.model({ name: "llama3.1" })
|
|
37
|
+
ollama.model({ name: "qwen2.5:14b", temperature: 0.2 })
|
|
38
|
+
ollama.model({ name: "llama3.2-vision", maxTokens: 1024 })
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Capabilities — what's auto-set
|
|
42
|
+
|
|
43
|
+
| Flag | Default |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `structuredOutput` | `true` (via Ollama's native `format` JSON-schema field) |
|
|
46
|
+
| `vision` | Inferred from model tag substring. `true` for `llava`, `bakllava`, `*-vision`, `moondream`, `minicpm-v`, `qwen2-vl`, `qwen2.5-vl`, `llama4`, `gemma3`; `false` otherwise. |
|
|
47
|
+
|
|
48
|
+
Explicit config always wins.
|
|
49
|
+
|
|
50
|
+
## System prompt & roles
|
|
51
|
+
|
|
52
|
+
Unlike Anthropic/Gemini/Bedrock, **Ollama keeps a first-class `system` role inside `messages`** — no hoisting. Neutral roles (`system`/`user`/`assistant`/`tool`) pass straight through.
|
|
53
|
+
|
|
54
|
+
## Tool calls
|
|
55
|
+
|
|
56
|
+
- Outgoing: neutral tools → `{ type: "function", function: { name, description, parameters } }`.
|
|
57
|
+
- Assistant tool calls → `tool_calls: [{ function: { name, arguments } }]` (Ollama has **no tool-call id**).
|
|
58
|
+
- Tool results (`role: "tool"`) → a `tool` message with `tool_name` set from `toolCallId` (Ollama matches a result to its call by name).
|
|
59
|
+
|
|
60
|
+
**Synthesized ids.** Because Ollama tool calls carry no id, the adapter sets neutral `id` = tool name. **Parallel calls to the same tool in one turn share an id** — a documented v1 limitation. Ollama reports `done_reason: "stop"` even when it called a tool; the adapter derives `finishReason: "tool_calls"` from tool-call presence.
|
|
61
|
+
|
|
62
|
+
## Structured output
|
|
63
|
+
|
|
64
|
+
Object-root `responseSchema` + `structuredOutput`-capable → `chat({ format: <schema> })` (Ollama's `format` accepts a JSON Schema object).
|
|
65
|
+
|
|
66
|
+
## Multipart messages (vision)
|
|
67
|
+
|
|
68
|
+
A multipart user message collapses to a single `content` string + an `images` array of **base64 strings**. `{ type: "image", source: { url } }` → **throws `InvalidRequestError`** (Ollama cannot fetch remote URLs). Resolve images to base64 first.
|
|
69
|
+
|
|
70
|
+
## Streaming
|
|
71
|
+
|
|
72
|
+
`model.stream()` drains `chat({ stream: true })` (an `AbortableAsyncIterator`). Each chunk's `message.content` → `{ type: "delta" }`; `message.tool_calls` are emitted as `{ type: "tool-call" }` **fully formed**. Terminal `{ type: "done", finishReason, usage }` — usage from the final (`done: true`) chunk's `prompt_eval_count` / `eval_count`.
|
|
73
|
+
|
|
74
|
+
**`options.signal` is honored** by calling the iterator's `abort()` (stream path; non-stream `complete()` ignores it — the agent still honors the signal at trip boundaries).
|
|
75
|
+
|
|
76
|
+
## Finish-reason mapping
|
|
77
|
+
|
|
78
|
+
`stop` → `stop` · `length` → `length` · `load` / unknown / null → `error`. `tool_calls` derived from tool-call presence.
|
|
79
|
+
|
|
80
|
+
## Embeddings
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const embedder = ollama.embedder({ name: "nomic-embed-text" });
|
|
84
|
+
const { vector } = await embedder.embed("Hello world");
|
|
85
|
+
const { vectors } = await embedder.embedMany(["a", "b"]); // single batched call
|
|
86
|
+
const truncated = ollama.embedder({ name: "mxbai-embed-large", dimensions: 512 });
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`client.embed` accepts a string array natively, so `embedMany` is **one request** (like the Gemini adapter). Usage comes from `prompt_eval_count` (reported as both `promptTokens` and `totalTokens`). Local Ollama runs without a prompt cache, so model usage has no `cachedTokens`.
|
|
90
|
+
|
|
91
|
+
`dimensions` is optional. When set it's forwarded as Ollama's `dimensions` truncation field (newer embedding models) and seeds `embedder.dimensions`; when omitted, `embedder.dimensions` starts at `0` and is resolved lazily from the first response's vector length, then cached.
|
|
92
|
+
|
|
93
|
+
## Errors
|
|
94
|
+
|
|
95
|
+
Wrapped into the typed `@warlock.js/ai` `AIError` hierarchy. The `ollama` client throws an internal `ResponseError` (`status_code` + message); transport failures surface as `fetch` `TypeError` with `ECONNREFUSED` cause:
|
|
96
|
+
|
|
97
|
+
- **Daemon-down (`ECONNREFUSED` / "fetch failed") → `ProviderError`** (operational "is Ollama running?", not a request defect)
|
|
98
|
+
- Timeouts → `ProviderTimeoutError`
|
|
99
|
+
- 401/403 → `ProviderAuthError`
|
|
100
|
+
- 429 → `ProviderRateLimitError`
|
|
101
|
+
- 4xx with context phrasing → `ContextLengthExceededError`, else `InvalidRequestError`
|
|
102
|
+
- 5xx → `ProviderError`
|
|
103
|
+
|
|
104
|
+
## Token counting
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
await ollama.count("some text") // approximate heuristic, offline
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## When NOT to use this skill
|
|
111
|
+
|
|
112
|
+
- Direct `ollama` client calls without going through `@warlock.js/ai` agents.
|
|
113
|
+
- OpenAI / Anthropic / Bedrock / Google models — those have their own adapter packages.
|
|
114
|
+
- An OpenAI-compatible Ollama gateway you specifically want to drive through the OpenAI protocol — use `@warlock.js/ai-openai` with `baseURL` instead.
|
|
115
|
+
|
|
116
|
+
## See also
|
|
117
|
+
|
|
118
|
+
- [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md)
|
|
119
|
+
- [`@warlock.js/ai/pick-ai-provider/SKILL.md`](@warlock.js/ai/pick-ai-provider/SKILL.md)
|
|
120
|
+
- [`@warlock.js/ai/embed-text/SKILL.md`](@warlock.js/ai/embed-text/SKILL.md)
|
|
121
|
+
|
|
122
|
+
|
package/llms.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Warlock AI Ollama
|
|
2
|
+
|
|
3
|
+
> Package: `@warlock.js/ai-ollama`
|
|
4
|
+
|
|
5
|
+
> Ollama adapter for @warlock.js/ai
|
|
6
|
+
|
|
7
|
+
## Skills
|
|
8
|
+
|
|
9
|
+
- [setup-ollama](@warlock.js/ai-ollama/setup-ollama/SKILL.md): Wire @warlock.js/ai-ollama — new OllamaSDK({host?, headers?}) for local / self-hosted Ollama via the official ollama client (not OpenAI-compat). chat + embed, daemon-down error handling. Triggers: `OllamaSDK`, `ollama.model`, `ollama.embedder`, `embedder.embedMany`, `ollama.count`, `host`, `headers`; "use ollama with warlock", "run llama3 locally", "self-hosted llama"; typical import `import { OllamaSDK } from "@warlock.js/ai-ollama"`. Skip: agent loop — `@warlock.js/ai/run-ai-agent/SKILL.md`; provider choice — `@warlock.js/ai/pick-ai-provider/SKILL.md`; embeddings core — `@warlock.js/ai/embed-text/SKILL.md`; siblings `@warlock.js/ai-openai`, `@warlock.js/ai-anthropic`, `@warlock.js/ai-google`; raw `ollama` npm, Vercel `@ai-sdk/ollama`; OpenAI-compat gateway via `@warlock.js/ai-openai` `baseURL`.
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@warlock.js/ai-ollama",
|
|
3
|
+
"description": "Ollama adapter for @warlock.js/ai",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"warlock",
|
|
6
|
+
"ai",
|
|
7
|
+
"ollama"
|
|
8
|
+
],
|
|
9
|
+
"author": "Hasan Zohdy",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/warlockjs/ai-ollama"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"ollama": "^0.6.3",
|
|
17
|
+
"@warlock.js/logger": "*"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@warlock.js/ai": "*"
|
|
21
|
+
},
|
|
22
|
+
"version": "4.1.1",
|
|
23
|
+
"main": "./cjs/index.cjs",
|
|
24
|
+
"module": "./esm/index.mjs",
|
|
25
|
+
"types": "./esm/index.d.mts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": {
|
|
29
|
+
"types": "./esm/index.d.mts",
|
|
30
|
+
"default": "./esm/index.mjs"
|
|
31
|
+
},
|
|
32
|
+
"require": {
|
|
33
|
+
"types": "./esm/index.d.mts",
|
|
34
|
+
"default": "./cjs/index.cjs"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/skills/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# `@warlock.js/ai-ollama` — skills index
|
|
2
|
+
|
|
3
|
+
Per-task skills. All cross-references use the form `@warlock.js/<pkg>/<skill>/SKILL.md`.
|
|
4
|
+
|
|
5
|
+
## Skills
|
|
6
|
+
|
|
7
|
+
### [`setup-ollama/`](./setup-ollama/SKILL.md)
|
|
8
|
+
|
|
9
|
+
Wire @warlock.js/ai-ollama — new OllamaSDK({host?, headers?}) for local / self-hosted Ollama via the official ollama client (not OpenAI-compat). chat + embed, daemon-down error handling. Load when wiring a local or self-hosted Ollama model into a @warlock.js agent.
|