@graphorin/provider-llamacpp-node 0.5.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/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # @graphorin/provider-llamacpp-node
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release. See the root `CHANGELOG.md` and the corresponding
6
+ changeset for the full list of features.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oleksiy Stepurenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # `@graphorin/provider-llamacpp-node`
2
+
3
+ > Companion package to
4
+ > [`@graphorin/provider`](../provider/README.md) — in-process GGUF
5
+ > execution for the
6
+ > [**Graphorin**](https://github.com/o-stepper/graphorin) framework.
7
+
8
+ Wraps `node-llama-cpp@^3.5` to load `.gguf` model files directly into
9
+ the same Node process. No daemon, no port to manage, no GPU contention
10
+ with other processes. Trust class is permanent `loopback` because the
11
+ model lives in the same trust boundary as the host process.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pnpm add @graphorin/provider-llamacpp-node node-llama-cpp
17
+ ```
18
+
19
+ ## Quick start
20
+
21
+ ```ts
22
+ import { llamaCppNodeAdapter } from '@graphorin/provider-llamacpp-node';
23
+ import { createProvider } from '@graphorin/provider';
24
+
25
+ const provider = createProvider(
26
+ llamaCppNodeAdapter({
27
+ modelPath: '/path/to/qwen2.5-7b-instruct-q4_k_m.gguf',
28
+ gpuLayers: 'auto',
29
+ }),
30
+ );
31
+ ```
32
+
33
+ ## Native token counting
34
+
35
+ ```ts
36
+ import { LlamaCppNativeCounter } from '@graphorin/provider-llamacpp-node';
37
+ import { setGlobalTokenCounter } from '@graphorin/provider/counters';
38
+
39
+ setGlobalTokenCounter(
40
+ new LlamaCppNativeCounter({
41
+ model: loadedGgufModel,
42
+ modelPath: '/path/to/qwen2.5-7b.gguf',
43
+ }),
44
+ );
45
+ ```
46
+
47
+ The counter wraps the GGUF tokenizer directly, which is strictly
48
+ tighter than the cl100k_base proxy used by the HTTP-shaped adapters.
49
+
50
+ ## HITL durable-resume tradeoff
51
+
52
+ The in-process adapter does **not** survive a process restart
53
+ mid-stream — the model context lives in the running process and is
54
+ lost on exit. For human-in-the-loop workflows that need durable
55
+ mid-stream resume across restarts, prefer one of the HTTP-shaped
56
+ adapters instead:
57
+
58
+ - `ollamaAdapter` — Ollama HTTP daemon
59
+ - `llamaCppServerAdapter` — upstream `llama-server` binary
60
+ - `openAICompatibleAdapter` — LMStudio / LocalAI / vLLM /
61
+ Together-style endpoints
62
+
63
+ ## GGUF model provenance
64
+
65
+ `.gguf` model files are not signed by default. Pull only from trusted
66
+ publishers and verify the SHA-256 of the downloaded file against the
67
+ publisher's manifest:
68
+
69
+ - `huggingface.co/ggml-org`
70
+ - `huggingface.co/TheBloke`
71
+ - `huggingface.co/bartowski`
72
+ - `huggingface.co/unsloth`
73
+ - `huggingface.co/Qwen` (official Qwen distributions)
74
+
75
+ Full provenance enforcement (allowlist + Sigstore signature
76
+ verification) is a future Graphorin work item; v0.1 documents the
77
+ discipline rather than enforcing it at runtime.
78
+
79
+ ## Project metadata
80
+
81
+ - **Project Graphorin** · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko
82
+ - Repository: <https://github.com/o-stepper/graphorin>
@@ -0,0 +1,55 @@
1
+ import { LlamaCppNodeRuntimeOverrides, LlamaModelInstance, LlamaSessionInstance } from "./runtime.js";
2
+ import { Provider, ProviderCapabilities, Sensitivity } from "@graphorin/core";
3
+
4
+ //#region src/adapter.d.ts
5
+
6
+ /**
7
+ * Options accepted by {@link llamaCppNodeAdapter}.
8
+ *
9
+ * @stable
10
+ */
11
+ interface LlamaCppNodeAdapterOptions {
12
+ /** Filesystem path to the `.gguf` model file. */
13
+ readonly modelPath: string;
14
+ /** Number of layers to offload to the GPU. Default `'auto'`. */
15
+ readonly gpuLayers?: number | 'auto';
16
+ /** Optional context-window override. */
17
+ readonly contextSize?: number;
18
+ /** Provider name attached to spans / log lines. */
19
+ readonly name?: string;
20
+ /** Capability declaration. Merged on top of the defaults table. */
21
+ readonly capabilities?: Partial<ProviderCapabilities>;
22
+ /** Sensitivity override (defaults to the loopback envelope). */
23
+ readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;
24
+ /**
25
+ * Test-only runtime override. When unset the adapter loads
26
+ * `node-llama-cpp` lazily on first call.
27
+ */
28
+ readonly runtimeOverrides?: LlamaCppNodeRuntimeOverrides;
29
+ /**
30
+ * Optional `model` override that short-circuits the
31
+ * `loadLlamaModule(...).loadModel(...)` flow. Tests pass a fixture
32
+ * shaped instance.
33
+ */
34
+ readonly modelOverride?: LlamaModelInstance;
35
+ /**
36
+ * Optional session factory override. When unset, the adapter builds a
37
+ * real session from the peer (PS-3): `model.createContext()` →
38
+ * `new LlamaChatSession({ contextSequence })`, streaming through
39
+ * `prompt(text, { onTextChunk })`. Overrides
40
+ * (`runtimeOverrides.createSession` or this option) keep the test
41
+ * seam.
42
+ */
43
+ readonly sessionFactory?: (model: LlamaModelInstance, system?: string) => Promise<LlamaSessionInstance>;
44
+ }
45
+ /**
46
+ * Build a Graphorin {@link Provider} backed by an in-process GGUF
47
+ * model. The first call lazily loads the `node-llama-cpp` peer + the
48
+ * model file; subsequent calls reuse the cached instances.
49
+ *
50
+ * @stable
51
+ */
52
+ declare function llamaCppNodeAdapter(options: LlamaCppNodeAdapterOptions): Provider;
53
+ //#endregion
54
+ export { LlamaCppNodeAdapterOptions, llamaCppNodeAdapter };
55
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","names":[],"sources":["../src/adapter.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;UA8CiB,0BAAA;;;;;;;;;;0BAUS,QAAQ;;gCAEF,cAAc;;;;;8BAKhB;;;;;;2BAMH;;;;;;;;;oCAUhB,wCAEJ,QAAQ;;;;;;;;;iBAsBC,mBAAA,UAA6B,6BAA6B"}
@@ -0,0 +1,255 @@
1
+ import { loadLlamaChatSessionCtor, loadLlamaModule } from "./runtime.js";
2
+ import { ProviderHttpError } from "@graphorin/provider/errors";
3
+ import { applyReasoningPolicy, resolveReasoningRetention } from "@graphorin/provider/reasoning";
4
+
5
+ //#region src/adapter.ts
6
+ /**
7
+ * Default sensitivity envelope for the in-process adapter — same as
8
+ * the loopback HTTP path.
9
+ *
10
+ * @stable
11
+ */
12
+ const LLAMA_CPP_NODE_ACCEPTS_SENSITIVITY = Object.freeze([
13
+ "public",
14
+ "internal",
15
+ "secret"
16
+ ]);
17
+ const DEFAULT_CAPABILITIES = {
18
+ streaming: true,
19
+ toolCalling: false,
20
+ parallelToolCalls: false,
21
+ multimodal: false,
22
+ structuredOutput: false,
23
+ reasoning: false,
24
+ contextWindow: 8192,
25
+ maxOutput: 4096,
26
+ reasoningContract: "optional"
27
+ };
28
+ /**
29
+ * Build a Graphorin {@link Provider} backed by an in-process GGUF
30
+ * model. The first call lazily loads the `node-llama-cpp` peer + the
31
+ * model file; subsequent calls reuse the cached instances.
32
+ *
33
+ * @stable
34
+ */
35
+ function llamaCppNodeAdapter(options) {
36
+ const providerName = options.name ?? `llama-cpp-node-${basename(options.modelPath)}`;
37
+ const capabilities = {
38
+ ...DEFAULT_CAPABILITIES,
39
+ ...options.capabilities
40
+ };
41
+ const acceptsSensitivity = options.acceptsSensitivity ?? LLAMA_CPP_NODE_ACCEPTS_SENSITIVITY;
42
+ let resolved = null;
43
+ let resolving = null;
44
+ const ensureModel = async () => {
45
+ if (resolved !== null) return resolved;
46
+ if (resolving !== null) return resolving;
47
+ resolving = (async () => {
48
+ resolved = { model: options.modelOverride ?? await resolveModel(options) };
49
+ resolving = null;
50
+ return resolved;
51
+ })();
52
+ return resolving;
53
+ };
54
+ const sessionFactory = options.sessionFactory ?? options.runtimeOverrides?.createSession ?? ((model, system) => defaultSessionFactory(model, system, options));
55
+ return {
56
+ name: providerName,
57
+ modelId: options.modelPath,
58
+ capabilities,
59
+ acceptsSensitivity,
60
+ stream(req) {
61
+ return streamLlamaCppNode(ensureModel, sessionFactory, providerName, options, applyLlamaCppNodePreflight(req, capabilities));
62
+ },
63
+ async generate(req) {
64
+ const events = streamLlamaCppNode(ensureModel, sessionFactory, providerName, options, applyLlamaCppNodePreflight(req, capabilities));
65
+ const collected = [];
66
+ let usage = {
67
+ promptTokens: 0,
68
+ completionTokens: 0,
69
+ totalTokens: 0
70
+ };
71
+ let finishReason = "stop";
72
+ let streamError;
73
+ for await (const event of events) {
74
+ if (event.type === "text-delta") collected.push(event.delta);
75
+ if (event.type === "error") streamError = event.error;
76
+ if (event.type === "finish") {
77
+ usage = event.usage;
78
+ finishReason = event.finishReason;
79
+ }
80
+ }
81
+ if (streamError !== void 0) throw new ProviderHttpError({
82
+ providerName,
83
+ status: 0,
84
+ message: streamError.message
85
+ });
86
+ return {
87
+ text: collected.join(""),
88
+ usage,
89
+ finishReason
90
+ };
91
+ }
92
+ };
93
+ }
94
+ function applyLlamaCppNodePreflight(req, capabilities) {
95
+ const retention = resolveReasoningRetention({
96
+ ...req.reasoningRetention !== void 0 ? { requested: req.reasoningRetention } : {},
97
+ ...capabilities.reasoningContract !== void 0 ? { contract: capabilities.reasoningContract } : {}
98
+ });
99
+ if (retention === "pass-through-all") return req;
100
+ const filtered = applyReasoningPolicy({
101
+ messages: req.messages,
102
+ retention
103
+ });
104
+ if (filtered === req.messages) return req;
105
+ return {
106
+ ...req,
107
+ messages: filtered
108
+ };
109
+ }
110
+ async function* streamLlamaCppNode(ensureModel, sessionFactory, providerName, options, req) {
111
+ const { model } = await ensureModel();
112
+ const session = await sessionFactory(model, typeof req.systemMessage === "string" ? req.systemMessage : void 0);
113
+ const prompt = renderPrompt(req);
114
+ const promptTokens = lengthOf(model.tokenize(prompt));
115
+ yield {
116
+ type: "stream-start",
117
+ metadata: {
118
+ providerName,
119
+ modelId: options.modelPath,
120
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
121
+ }
122
+ };
123
+ let completionTokens = 0;
124
+ let errored = false;
125
+ try {
126
+ const streamOptions = {};
127
+ if (req.signal !== void 0) streamOptions.signal = req.signal;
128
+ if (req.maxTokens !== void 0) streamOptions.maxTokens = req.maxTokens;
129
+ if (req.temperature !== void 0) streamOptions.temperature = req.temperature;
130
+ for await (const piece of session.promptStreamingResponse(prompt, streamOptions)) {
131
+ if (typeof piece !== "string" || piece.length === 0) continue;
132
+ completionTokens += lengthOf(model.tokenize(piece));
133
+ yield {
134
+ type: "text-delta",
135
+ delta: piece
136
+ };
137
+ if (req.signal?.aborted) break;
138
+ }
139
+ } catch (err) {
140
+ errored = true;
141
+ yield {
142
+ type: "error",
143
+ error: {
144
+ kind: "unknown",
145
+ message: err.message
146
+ }
147
+ };
148
+ }
149
+ yield {
150
+ type: "finish",
151
+ finishReason: errored ? "error" : "stop",
152
+ usage: {
153
+ promptTokens,
154
+ completionTokens,
155
+ totalTokens: promptTokens + completionTokens
156
+ }
157
+ };
158
+ }
159
+ async function resolveModel(options) {
160
+ return (await loadLlamaModule(options.runtimeOverrides)).loadModel({
161
+ modelPath: options.modelPath,
162
+ ...options.gpuLayers !== void 0 ? { gpuLayers: options.gpuLayers } : {}
163
+ });
164
+ }
165
+ /**
166
+ * The REAL default session factory (PS-3): `model.createContext()` →
167
+ * `context.getSequence()` → `new LlamaChatSession({ contextSequence })`
168
+ * from the lazily-loaded peer, adapting its callback-streaming
169
+ * `prompt(text, { onTextChunk })` to the adapter's
170
+ * `promptStreamingResponse` AsyncIterable contract.
171
+ */
172
+ async function defaultSessionFactory(model, system, options) {
173
+ const session = new (await (loadLlamaChatSessionCtor(options.runtimeOverrides)))({
174
+ contextSequence: (await model.createContext(options.contextSize !== void 0 ? { contextSize: options.contextSize } : void 0)).getSequence(),
175
+ ...system !== void 0 ? { systemPrompt: system } : {}
176
+ });
177
+ return { promptStreamingResponse(prompt, streamOptions) {
178
+ return promptToIterable(session, prompt, streamOptions);
179
+ } };
180
+ }
181
+ /**
182
+ * Bridge the peer's callback-streaming `prompt(...)` into the
183
+ * AsyncIterable the adapter consumes. A rejection from `prompt`
184
+ * rejects the pending/next iteration so the stream's error path
185
+ * (PS-4) observes it.
186
+ */
187
+ function promptToIterable(session, prompt, streamOptions) {
188
+ const queue = [];
189
+ let done = false;
190
+ let failure;
191
+ let wake = null;
192
+ const notify = () => {
193
+ if (wake !== null) {
194
+ const w = wake;
195
+ wake = null;
196
+ w();
197
+ }
198
+ };
199
+ session.prompt(prompt, {
200
+ ...streamOptions?.signal !== void 0 ? { signal: streamOptions.signal } : {},
201
+ ...streamOptions?.maxTokens !== void 0 ? { maxTokens: streamOptions.maxTokens } : {},
202
+ ...streamOptions?.temperature !== void 0 ? { temperature: streamOptions.temperature } : {},
203
+ onTextChunk: (chunk) => {
204
+ if (typeof chunk === "string" && chunk.length > 0) queue.push(chunk);
205
+ notify();
206
+ }
207
+ }).then(() => {
208
+ done = true;
209
+ notify();
210
+ }, (err) => {
211
+ failure = err instanceof Error ? err : new Error(String(err));
212
+ done = true;
213
+ notify();
214
+ });
215
+ return { [Symbol.asyncIterator]() {
216
+ return { async next() {
217
+ for (;;) {
218
+ const head = queue.shift();
219
+ if (head !== void 0) return {
220
+ done: false,
221
+ value: head
222
+ };
223
+ if (failure !== void 0) throw failure;
224
+ if (done) return {
225
+ done: true,
226
+ value: void 0
227
+ };
228
+ await new Promise((resolve) => {
229
+ wake = resolve;
230
+ });
231
+ }
232
+ } };
233
+ } };
234
+ }
235
+ function renderPrompt(req) {
236
+ const parts = [];
237
+ if (req.systemMessage !== void 0) parts.push(`[system] ${req.systemMessage}`);
238
+ for (const msg of req.messages) {
239
+ const text = typeof msg.content === "string" ? msg.content : msg.content.map((p) => p.type === "text" ? p.text : p.type === "reasoning" ? p.text : "").join("");
240
+ parts.push(`[${msg.role}] ${text}`);
241
+ }
242
+ return parts.join("\n");
243
+ }
244
+ function basename(path) {
245
+ const idx = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
246
+ return idx === -1 ? path : path.slice(idx + 1);
247
+ }
248
+ function lengthOf(tokens) {
249
+ if (tokens === null || tokens === void 0) return 0;
250
+ return tokens.length ?? 0;
251
+ }
252
+
253
+ //#endregion
254
+ export { llamaCppNodeAdapter };
255
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","names":["LLAMA_CPP_NODE_ACCEPTS_SENSITIVITY: ReadonlyArray<Sensitivity>","DEFAULT_CAPABILITIES: ProviderCapabilities","capabilities: ProviderCapabilities","resolved: { model: LlamaModelInstance } | null","resolving: Promise<{ model: LlamaModelInstance }> | null","collected: string[]","usage: Usage","finishReason: ProviderResponse['finishReason']","streamError: { message: string } | undefined","streamOptions: { signal?: AbortSignal; maxTokens?: number; temperature?: number }","queue: string[]","failure: unknown","wake: (() => void) | null","parts: string[]"],"sources":["../src/adapter.ts"],"sourcesContent":["/**\n * `llamaCppNodeAdapter` — in-process GGUF adapter built on\n * `node-llama-cpp@^3.5`. The adapter declares `trust: 'loopback'`\n * permanently because the model lives in the same trust boundary as\n * the host process.\n *\n * @packageDocumentation\n */\n\nimport type {\n Provider,\n ProviderCapabilities,\n ProviderEvent,\n ProviderRequest,\n ProviderResponse,\n Sensitivity,\n Usage,\n} from '@graphorin/core';\nimport { ProviderHttpError } from '@graphorin/provider/errors';\nimport { applyReasoningPolicy, resolveReasoningRetention } from '@graphorin/provider/reasoning';\nimport {\n type LlamaChatSessionPeer,\n type LlamaCppNodeRuntimeOverrides,\n type LlamaModelInstance,\n type LlamaSessionInstance,\n loadLlamaChatSessionCtor,\n loadLlamaModule,\n} from './runtime.js';\n\n/**\n * Default sensitivity envelope for the in-process adapter — same as\n * the loopback HTTP path.\n *\n * @stable\n */\nexport const LLAMA_CPP_NODE_ACCEPTS_SENSITIVITY: ReadonlyArray<Sensitivity> = Object.freeze([\n 'public',\n 'internal',\n 'secret',\n]);\n\n/**\n * Options accepted by {@link llamaCppNodeAdapter}.\n *\n * @stable\n */\nexport interface LlamaCppNodeAdapterOptions {\n /** Filesystem path to the `.gguf` model file. */\n readonly modelPath: string;\n /** Number of layers to offload to the GPU. Default `'auto'`. */\n readonly gpuLayers?: number | 'auto';\n /** Optional context-window override. */\n readonly contextSize?: number;\n /** Provider name attached to spans / log lines. */\n readonly name?: string;\n /** Capability declaration. Merged on top of the defaults table. */\n readonly capabilities?: Partial<ProviderCapabilities>;\n /** Sensitivity override (defaults to the loopback envelope). */\n readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;\n /**\n * Test-only runtime override. When unset the adapter loads\n * `node-llama-cpp` lazily on first call.\n */\n readonly runtimeOverrides?: LlamaCppNodeRuntimeOverrides;\n /**\n * Optional `model` override that short-circuits the\n * `loadLlamaModule(...).loadModel(...)` flow. Tests pass a fixture\n * shaped instance.\n */\n readonly modelOverride?: LlamaModelInstance;\n /**\n * Optional session factory override. When unset, the adapter builds a\n * real session from the peer (PS-3): `model.createContext()` →\n * `new LlamaChatSession({ contextSequence })`, streaming through\n * `prompt(text, { onTextChunk })`. Overrides\n * (`runtimeOverrides.createSession` or this option) keep the test\n * seam.\n */\n readonly sessionFactory?: (\n model: LlamaModelInstance,\n system?: string,\n ) => Promise<LlamaSessionInstance>;\n}\n\nconst DEFAULT_CAPABILITIES: ProviderCapabilities = {\n streaming: true,\n toolCalling: false,\n parallelToolCalls: false,\n multimodal: false,\n structuredOutput: false,\n reasoning: false,\n contextWindow: 8_192,\n maxOutput: 4_096,\n reasoningContract: 'optional',\n};\n\n/**\n * Build a Graphorin {@link Provider} backed by an in-process GGUF\n * model. The first call lazily loads the `node-llama-cpp` peer + the\n * model file; subsequent calls reuse the cached instances.\n *\n * @stable\n */\nexport function llamaCppNodeAdapter(options: LlamaCppNodeAdapterOptions): Provider {\n const providerName = options.name ?? `llama-cpp-node-${basename(options.modelPath)}`;\n const capabilities: ProviderCapabilities = {\n ...DEFAULT_CAPABILITIES,\n ...options.capabilities,\n };\n const acceptsSensitivity = options.acceptsSensitivity ?? LLAMA_CPP_NODE_ACCEPTS_SENSITIVITY;\n let resolved: { model: LlamaModelInstance } | null = null;\n let resolving: Promise<{ model: LlamaModelInstance }> | null = null;\n const ensureModel = async (): Promise<{ model: LlamaModelInstance }> => {\n if (resolved !== null) return resolved;\n if (resolving !== null) return resolving;\n resolving = (async () => {\n const model = options.modelOverride ?? (await resolveModel(options));\n resolved = { model };\n resolving = null;\n return resolved;\n })();\n return resolving;\n };\n const sessionFactory =\n options.sessionFactory ??\n options.runtimeOverrides?.createSession ??\n ((model: LlamaModelInstance, system?: string) => defaultSessionFactory(model, system, options));\n return {\n name: providerName,\n modelId: options.modelPath,\n capabilities,\n acceptsSensitivity,\n stream(req) {\n return streamLlamaCppNode(\n ensureModel,\n sessionFactory,\n providerName,\n options,\n applyLlamaCppNodePreflight(req, capabilities),\n );\n },\n async generate(req) {\n const events = streamLlamaCppNode(\n ensureModel,\n sessionFactory,\n providerName,\n options,\n applyLlamaCppNodePreflight(req, capabilities),\n );\n const collected: string[] = [];\n let usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n let finishReason: ProviderResponse['finishReason'] = 'stop';\n let streamError: { message: string } | undefined;\n for await (const event of events) {\n if (event.type === 'text-delta') collected.push(event.delta);\n if (event.type === 'error') streamError = event.error;\n if (event.type === 'finish') {\n usage = event.usage;\n finishReason = event.finishReason;\n }\n }\n // PS-4: a swallowed mid-stream error returned truncated text\n // indistinguishable from success — and a never-throwing\n // generate() bypasses withRetry / withFallback entirely.\n if (streamError !== undefined) {\n throw new ProviderHttpError({\n providerName,\n status: 0,\n message: streamError.message,\n });\n }\n return {\n text: collected.join(''),\n usage,\n finishReason,\n };\n },\n };\n}\n\nfunction applyLlamaCppNodePreflight(\n req: ProviderRequest,\n capabilities: ProviderCapabilities,\n): ProviderRequest {\n const retention = resolveReasoningRetention({\n ...(req.reasoningRetention !== undefined ? { requested: req.reasoningRetention } : {}),\n ...(capabilities.reasoningContract !== undefined\n ? { contract: capabilities.reasoningContract }\n : {}),\n });\n if (retention === 'pass-through-all') return req;\n const filtered = applyReasoningPolicy({ messages: req.messages, retention });\n if (filtered === req.messages) return req;\n return { ...req, messages: filtered };\n}\n\nasync function* streamLlamaCppNode(\n ensureModel: () => Promise<{ model: LlamaModelInstance }>,\n sessionFactory: (model: LlamaModelInstance, system?: string) => Promise<LlamaSessionInstance>,\n providerName: string,\n options: LlamaCppNodeAdapterOptions,\n req: ProviderRequest,\n): AsyncIterable<ProviderEvent> {\n const { model } = await ensureModel();\n const session = await sessionFactory(\n model,\n typeof req.systemMessage === 'string' ? req.systemMessage : undefined,\n );\n const prompt = renderPrompt(req);\n const promptTokens = lengthOf(model.tokenize(prompt));\n yield {\n type: 'stream-start',\n metadata: {\n providerName,\n modelId: options.modelPath,\n createdAt: new Date().toISOString(),\n },\n };\n let completionTokens = 0;\n let errored = false;\n try {\n const streamOptions: { signal?: AbortSignal; maxTokens?: number; temperature?: number } = {};\n if (req.signal !== undefined) streamOptions.signal = req.signal;\n if (req.maxTokens !== undefined) streamOptions.maxTokens = req.maxTokens;\n if (req.temperature !== undefined) streamOptions.temperature = req.temperature;\n for await (const piece of session.promptStreamingResponse(prompt, streamOptions)) {\n if (typeof piece !== 'string' || piece.length === 0) continue;\n completionTokens += lengthOf(model.tokenize(piece));\n yield { type: 'text-delta', delta: piece };\n if (req.signal?.aborted) break;\n }\n } catch (err) {\n errored = true;\n yield { type: 'error', error: { kind: 'unknown', message: (err as Error).message } };\n }\n yield {\n type: 'finish',\n // PS-4: a mid-stream failure must not masquerade as a clean stop —\n // partial text would be indistinguishable from success.\n finishReason: errored ? 'error' : 'stop',\n usage: {\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n },\n };\n}\n\nasync function resolveModel(options: LlamaCppNodeAdapterOptions): Promise<LlamaModelInstance> {\n const llama = await loadLlamaModule(options.runtimeOverrides);\n return llama.loadModel({\n modelPath: options.modelPath,\n ...(options.gpuLayers !== undefined ? { gpuLayers: options.gpuLayers } : {}),\n });\n}\n\n/**\n * The REAL default session factory (PS-3): `model.createContext()` →\n * `context.getSequence()` → `new LlamaChatSession({ contextSequence })`\n * from the lazily-loaded peer, adapting its callback-streaming\n * `prompt(text, { onTextChunk })` to the adapter's\n * `promptStreamingResponse` AsyncIterable contract.\n */\nasync function defaultSessionFactory(\n model: LlamaModelInstance,\n system: string | undefined,\n options: LlamaCppNodeAdapterOptions,\n): Promise<LlamaSessionInstance> {\n const ChatSession = await loadLlamaChatSessionCtor(options.runtimeOverrides);\n const context = await model.createContext(\n options.contextSize !== undefined ? { contextSize: options.contextSize } : undefined,\n );\n const sequence = context.getSequence();\n const session = new ChatSession({\n contextSequence: sequence,\n ...(system !== undefined ? { systemPrompt: system } : {}),\n });\n return {\n promptStreamingResponse(prompt, streamOptions) {\n return promptToIterable(session, prompt, streamOptions);\n },\n };\n}\n\n/**\n * Bridge the peer's callback-streaming `prompt(...)` into the\n * AsyncIterable the adapter consumes. A rejection from `prompt`\n * rejects the pending/next iteration so the stream's error path\n * (PS-4) observes it.\n */\nfunction promptToIterable(\n session: LlamaChatSessionPeer,\n prompt: string,\n streamOptions?: {\n readonly signal?: AbortSignal;\n readonly maxTokens?: number;\n readonly temperature?: number;\n },\n): AsyncIterable<string> {\n const queue: string[] = [];\n let done = false;\n let failure: unknown;\n let wake: (() => void) | null = null;\n const notify = (): void => {\n if (wake !== null) {\n const w = wake;\n wake = null;\n w();\n }\n };\n void session\n .prompt(prompt, {\n ...(streamOptions?.signal !== undefined ? { signal: streamOptions.signal } : {}),\n ...(streamOptions?.maxTokens !== undefined ? { maxTokens: streamOptions.maxTokens } : {}),\n ...(streamOptions?.temperature !== undefined\n ? { temperature: streamOptions.temperature }\n : {}),\n onTextChunk: (chunk: string) => {\n if (typeof chunk === 'string' && chunk.length > 0) queue.push(chunk);\n notify();\n },\n })\n .then(\n () => {\n done = true;\n notify();\n },\n (err: unknown) => {\n failure = err instanceof Error ? err : new Error(String(err));\n done = true;\n notify();\n },\n );\n return {\n [Symbol.asyncIterator](): AsyncIterator<string> {\n return {\n async next(): Promise<IteratorResult<string>> {\n for (;;) {\n const head = queue.shift();\n if (head !== undefined) return { done: false, value: head };\n if (failure !== undefined) throw failure;\n if (done) return { done: true, value: undefined };\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n }\n },\n };\n },\n };\n}\n\nfunction renderPrompt(req: ProviderRequest): string {\n const parts: string[] = [];\n if (req.systemMessage !== undefined) parts.push(`[system] ${req.systemMessage}`);\n for (const msg of req.messages) {\n const text =\n typeof msg.content === 'string'\n ? msg.content\n : msg.content\n .map((p) => (p.type === 'text' ? p.text : p.type === 'reasoning' ? p.text : ''))\n .join('');\n parts.push(`[${msg.role}] ${text}`);\n }\n return parts.join('\\n');\n}\n\nfunction basename(path: string): string {\n const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\\\'));\n return idx === -1 ? path : path.slice(idx + 1);\n}\n\nfunction lengthOf(tokens: readonly number[] | Uint32Array | Uint8Array | null | undefined): number {\n if (tokens === null || tokens === undefined) return 0;\n return (tokens as { length?: number }).length ?? 0;\n}\n"],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import { LlamaModelInstance } from "./runtime.js";
2
+ import { Message, TokenCounter } from "@graphorin/core";
3
+
4
+ //#region src/counter.d.ts
5
+
6
+ /**
7
+ * Options for {@link LlamaCppNativeCounter}.
8
+ *
9
+ * @stable
10
+ */
11
+ interface LlamaCppNativeCounterOptions {
12
+ readonly model: LlamaModelInstance;
13
+ readonly modelPath?: string;
14
+ readonly id?: string;
15
+ }
16
+ /**
17
+ * Counter that delegates to `model.tokenize(text)` from the loaded
18
+ * GGUF instance. Cache invalidation is keyed on the model file path
19
+ * (when supplied) so swapping models invalidates per-message caches
20
+ * upstream.
21
+ *
22
+ * @stable
23
+ */
24
+ declare class LlamaCppNativeCounter implements TokenCounter {
25
+ #private;
26
+ readonly id: string;
27
+ readonly version: string;
28
+ constructor(options: LlamaCppNativeCounterOptions);
29
+ count(messages: ReadonlyArray<Message>): Promise<number>;
30
+ countText(text: string): Promise<number>;
31
+ }
32
+ //#endregion
33
+ export { LlamaCppNativeCounter, LlamaCppNativeCounterOptions };
34
+ //# sourceMappingURL=counter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"counter.d.ts","names":[],"sources":["../src/counter.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;UAmBiB,4BAAA;kBACC;;;;;;;;;;;;cAaL,qBAAA,YAAiC;;;;uBAKvB;kBAOC,cAAc,WAAW;2BAShB"}
@@ -0,0 +1,50 @@
1
+ import { serialiseMessageForCount, serializedToString } from "@graphorin/provider/counters";
2
+
3
+ //#region src/counter.ts
4
+ /**
5
+ * Counter that delegates to `model.tokenize(text)` from the loaded
6
+ * GGUF instance. Cache invalidation is keyed on the model file path
7
+ * (when supplied) so swapping models invalidates per-message caches
8
+ * upstream.
9
+ *
10
+ * @stable
11
+ */
12
+ var LlamaCppNativeCounter = class {
13
+ id;
14
+ version;
15
+ #model;
16
+ constructor(options) {
17
+ this.#model = options.model;
18
+ const fingerprint = options.modelPath !== void 0 ? hash(options.modelPath) : "unknown";
19
+ this.id = options.id ?? `llama-cpp-native@${fingerprint}`;
20
+ this.version = `llama-cpp-native-${fingerprint}-v1`;
21
+ }
22
+ async count(messages) {
23
+ let total = 0;
24
+ for (const msg of messages) {
25
+ const serialised = serialiseMessageForCount(msg);
26
+ total += this.#tokenLengthOf(serializedToString(serialised));
27
+ }
28
+ return total;
29
+ }
30
+ async countText(text) {
31
+ if (text.length === 0) return 0;
32
+ return this.#tokenLengthOf(text);
33
+ }
34
+ #tokenLengthOf(text) {
35
+ if (text.length === 0) return 0;
36
+ const tokens = this.#model.tokenize(text);
37
+ if (tokens === null || tokens === void 0) return 0;
38
+ if (typeof tokens.length === "number") return tokens.length;
39
+ return 0;
40
+ }
41
+ };
42
+ function hash(value) {
43
+ let h = 0;
44
+ for (let i = 0; i < value.length; i++) h = h * 31 + value.charCodeAt(i) | 0;
45
+ return Math.abs(h).toString(16);
46
+ }
47
+
48
+ //#endregion
49
+ export { LlamaCppNativeCounter };
50
+ //# sourceMappingURL=counter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"counter.js","names":["#model","#tokenLengthOf"],"sources":["../src/counter.ts"],"sourcesContent":["/**\n * `LlamaCppNativeCounter` — token counter that wraps the loaded\n * `node-llama-cpp` model's `tokenize(text)` function. The counter is\n * strictly tighter than the cl100k_base proxy because it uses the\n * exact tokenizer the GGUF file embeds.\n *\n * @packageDocumentation\n */\n\nimport type { Message, TokenCounter } from '@graphorin/core';\nimport { serialiseMessageForCount, serializedToString } from '@graphorin/provider/counters';\n\nimport type { LlamaModelInstance } from './runtime.js';\n\n/**\n * Options for {@link LlamaCppNativeCounter}.\n *\n * @stable\n */\nexport interface LlamaCppNativeCounterOptions {\n readonly model: LlamaModelInstance;\n readonly modelPath?: string;\n readonly id?: string;\n}\n\n/**\n * Counter that delegates to `model.tokenize(text)` from the loaded\n * GGUF instance. Cache invalidation is keyed on the model file path\n * (when supplied) so swapping models invalidates per-message caches\n * upstream.\n *\n * @stable\n */\nexport class LlamaCppNativeCounter implements TokenCounter {\n readonly id: string;\n readonly version: string;\n readonly #model: LlamaModelInstance;\n\n constructor(options: LlamaCppNativeCounterOptions) {\n this.#model = options.model;\n const fingerprint = options.modelPath !== undefined ? hash(options.modelPath) : 'unknown';\n this.id = options.id ?? `llama-cpp-native@${fingerprint}`;\n this.version = `llama-cpp-native-${fingerprint}-v1`;\n }\n\n async count(messages: ReadonlyArray<Message>): Promise<number> {\n let total = 0;\n for (const msg of messages) {\n const serialised = serialiseMessageForCount(msg);\n total += this.#tokenLengthOf(serializedToString(serialised));\n }\n return total;\n }\n\n async countText(text: string): Promise<number> {\n if (text.length === 0) return 0;\n return this.#tokenLengthOf(text);\n }\n\n #tokenLengthOf(text: string): number {\n if (text.length === 0) return 0;\n const tokens = this.#model.tokenize(text);\n if (tokens === null || tokens === undefined) return 0;\n if (typeof (tokens as { length?: number }).length === 'number') {\n return (tokens as { length: number }).length;\n }\n return 0;\n }\n}\n\nfunction hash(value: string): string {\n // Lightweight deterministic fingerprint — the framework only needs\n // it for cache invalidation, not for cryptographic purposes.\n let h = 0;\n for (let i = 0; i < value.length; i++) {\n h = (h * 31 + value.charCodeAt(i)) | 0;\n }\n return Math.abs(h).toString(16);\n}\n"],"mappings":";;;;;;;;;;;AAiCA,IAAa,wBAAb,MAA2D;CACzD,AAAS;CACT,AAAS;CACT,CAASA;CAET,YAAY,SAAuC;AACjD,QAAKA,QAAS,QAAQ;EACtB,MAAM,cAAc,QAAQ,cAAc,SAAY,KAAK,QAAQ,UAAU,GAAG;AAChF,OAAK,KAAK,QAAQ,MAAM,oBAAoB;AAC5C,OAAK,UAAU,oBAAoB,YAAY;;CAGjD,MAAM,MAAM,UAAmD;EAC7D,IAAI,QAAQ;AACZ,OAAK,MAAM,OAAO,UAAU;GAC1B,MAAM,aAAa,yBAAyB,IAAI;AAChD,YAAS,MAAKC,cAAe,mBAAmB,WAAW,CAAC;;AAE9D,SAAO;;CAGT,MAAM,UAAU,MAA+B;AAC7C,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,SAAO,MAAKA,cAAe,KAAK;;CAGlC,eAAe,MAAsB;AACnC,MAAI,KAAK,WAAW,EAAG,QAAO;EAC9B,MAAM,SAAS,MAAKD,MAAO,SAAS,KAAK;AACzC,MAAI,WAAW,QAAQ,WAAW,OAAW,QAAO;AACpD,MAAI,OAAQ,OAA+B,WAAW,SACpD,QAAQ,OAA8B;AAExC,SAAO;;;AAIX,SAAS,KAAK,OAAuB;CAGnC,IAAI,IAAI;AACR,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,IAChC,KAAK,IAAI,KAAK,MAAM,WAAW,EAAE,GAAI;AAEvC,QAAO,KAAK,IAAI,EAAE,CAAC,SAAS,GAAG"}
@@ -0,0 +1,32 @@
1
+ import { LlamaCppNodeRuntimeOverrides, LlamaInstance, LlamaModelInstance, LlamaSessionInstance } from "./runtime.js";
2
+ import { LlamaCppNodeAdapterOptions, llamaCppNodeAdapter } from "./adapter.js";
3
+ import { LlamaCppNativeCounter, LlamaCppNativeCounterOptions } from "./counter.js";
4
+
5
+ //#region src/index.d.ts
6
+
7
+ /**
8
+ * @graphorin/provider-llamacpp-node — in-process GGUF execution
9
+ * adapter for the Graphorin framework. The package wraps
10
+ * `node-llama-cpp@^3.5` to load `.gguf` model files directly into the
11
+ * same Node process — no daemon, no port to manage, no GPU contention
12
+ * with other processes.
13
+ *
14
+ * The adapter declares `trust: 'loopback'` permanently because the
15
+ * model lives in the same trust boundary as the host process; the
16
+ * symmetry mirrors `@graphorin/embedder-transformersjs` (in-process
17
+ * embedder; same trust boundary).
18
+ *
19
+ * The companion package is operationally simpler than the HTTP-shaped
20
+ * adapters but does NOT survive a process restart mid-stream — the
21
+ * model context lives in the process and is lost on exit. For HITL
22
+ * durable mid-stream resume, one of the HTTP-shaped adapters
23
+ * (`ollamaAdapter`, `llamaCppServerAdapter`, `openAICompatibleAdapter`)
24
+ * is the better choice.
25
+ *
26
+ * @packageDocumentation
27
+ */
28
+ /** Canonical version constant. Mirrors the `package.json` version. */
29
+ declare const VERSION = "0.5.0";
30
+ //#endregion
31
+ export { LlamaCppNativeCounter, type LlamaCppNativeCounterOptions, type LlamaCppNodeAdapterOptions, type LlamaCppNodeRuntimeOverrides, type LlamaInstance, type LlamaModelInstance, type LlamaSessionInstance, VERSION, llamaCppNodeAdapter };
32
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;AAuBA;;;;;;;;;;;;;;;;;cAAa,OAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ import { llamaCppNodeAdapter } from "./adapter.js";
2
+ import { LlamaCppNativeCounter } from "./counter.js";
3
+
4
+ //#region src/index.ts
5
+ /**
6
+ * @graphorin/provider-llamacpp-node — in-process GGUF execution
7
+ * adapter for the Graphorin framework. The package wraps
8
+ * `node-llama-cpp@^3.5` to load `.gguf` model files directly into the
9
+ * same Node process — no daemon, no port to manage, no GPU contention
10
+ * with other processes.
11
+ *
12
+ * The adapter declares `trust: 'loopback'` permanently because the
13
+ * model lives in the same trust boundary as the host process; the
14
+ * symmetry mirrors `@graphorin/embedder-transformersjs` (in-process
15
+ * embedder; same trust boundary).
16
+ *
17
+ * The companion package is operationally simpler than the HTTP-shaped
18
+ * adapters but does NOT survive a process restart mid-stream — the
19
+ * model context lives in the process and is lost on exit. For HITL
20
+ * durable mid-stream resume, one of the HTTP-shaped adapters
21
+ * (`ollamaAdapter`, `llamaCppServerAdapter`, `openAICompatibleAdapter`)
22
+ * is the better choice.
23
+ *
24
+ * @packageDocumentation
25
+ */
26
+ /** Canonical version constant. Mirrors the `package.json` version. */
27
+ const VERSION = "0.5.0";
28
+
29
+ //#endregion
30
+ export { LlamaCppNativeCounter, VERSION, llamaCppNodeAdapter };
31
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @graphorin/provider-llamacpp-node — in-process GGUF execution\n * adapter for the Graphorin framework. The package wraps\n * `node-llama-cpp@^3.5` to load `.gguf` model files directly into the\n * same Node process — no daemon, no port to manage, no GPU contention\n * with other processes.\n *\n * The adapter declares `trust: 'loopback'` permanently because the\n * model lives in the same trust boundary as the host process; the\n * symmetry mirrors `@graphorin/embedder-transformersjs` (in-process\n * embedder; same trust boundary).\n *\n * The companion package is operationally simpler than the HTTP-shaped\n * adapters but does NOT survive a process restart mid-stream — the\n * model context lives in the process and is lost on exit. For HITL\n * durable mid-stream resume, one of the HTTP-shaped adapters\n * (`ollamaAdapter`, `llamaCppServerAdapter`, `openAICompatibleAdapter`)\n * is the better choice.\n *\n * @packageDocumentation\n */\n\n/** Canonical version constant. Mirrors the `package.json` version. */\nexport const VERSION = '0.5.0';\n\nexport { type LlamaCppNodeAdapterOptions, llamaCppNodeAdapter } from './adapter.js';\nexport {\n LlamaCppNativeCounter,\n type LlamaCppNativeCounterOptions,\n} from './counter.js';\nexport type {\n LlamaCppNodeRuntimeOverrides,\n LlamaInstance,\n LlamaModelInstance,\n LlamaSessionInstance,\n} from './runtime.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,UAAU"}
@@ -0,0 +1,94 @@
1
+ //#region src/runtime.d.ts
2
+ /**
3
+ * Loose structural shapes covering the slice of `node-llama-cpp` we
4
+ * use. Re-declared here to avoid pulling the heavy native peer at
5
+ * type-check time.
6
+ *
7
+ * @internal
8
+ */
9
+ /**
10
+ * `Llama` engine instance (returned by `getLlama()`).
11
+ *
12
+ * @internal
13
+ */
14
+ interface LlamaInstance {
15
+ loadModel(args: {
16
+ modelPath: string;
17
+ gpuLayers?: number | 'auto';
18
+ }): Promise<LlamaModelInstance>;
19
+ }
20
+ /**
21
+ * Loaded GGUF model.
22
+ *
23
+ * @internal
24
+ */
25
+ interface LlamaModelInstance {
26
+ readonly trainContextSize?: number;
27
+ tokenize(text: string): readonly number[] | Uint32Array | Uint8Array;
28
+ createContext(args?: {
29
+ contextSize?: number;
30
+ }): Promise<{
31
+ getSequence(): {
32
+ dispose?: () => void;
33
+ };
34
+ dispose?: () => void;
35
+ }>;
36
+ dispose?(): Promise<void>;
37
+ }
38
+ /**
39
+ * Loaded chat session capable of streaming responses.
40
+ *
41
+ * @internal
42
+ */
43
+ interface LlamaSessionInstance {
44
+ promptStreamingResponse(prompt: string, options?: {
45
+ readonly signal?: AbortSignal;
46
+ readonly maxTokens?: number;
47
+ readonly temperature?: number;
48
+ }): AsyncIterable<string>;
49
+ }
50
+ /**
51
+ * Test-only shape for injecting fixture-driven runtime behaviour.
52
+ *
53
+ * @stable
54
+ */
55
+ interface LlamaCppNodeRuntimeOverrides {
56
+ /** Returns a `LlamaInstance` (the result of `getLlama()`). */
57
+ readonly getLlama?: () => Promise<LlamaInstance>;
58
+ /**
59
+ * Build a streaming chat session against an already-loaded model
60
+ * instance. Used by the adapter to wire `model.tokenize` and
61
+ * `session.promptStreamingResponse` to the per-test fixture.
62
+ */
63
+ readonly createSession?: (model: LlamaModelInstance, system?: string) => Promise<LlamaSessionInstance>;
64
+ /**
65
+ * Override the `LlamaChatSession` constructor used by the REAL
66
+ * default session factory (PS-3). Tests stub it; production loads it
67
+ * from the `node-llama-cpp` peer.
68
+ */
69
+ readonly LlamaChatSession?: LlamaChatSessionCtor;
70
+ }
71
+ /**
72
+ * Structural slice of the peer's `LlamaChatSession` class used by the
73
+ * default session factory (PS-3): `prompt(text, { onTextChunk })`
74
+ * resolves with the full response while streaming chunks through the
75
+ * callback.
76
+ *
77
+ * @internal
78
+ */
79
+ interface LlamaChatSessionPeer {
80
+ prompt(text: string, options?: {
81
+ readonly signal?: AbortSignal;
82
+ readonly maxTokens?: number;
83
+ readonly temperature?: number;
84
+ readonly onTextChunk?: (chunk: string) => void;
85
+ }): Promise<string>;
86
+ }
87
+ /** @internal */
88
+ type LlamaChatSessionCtor = new (args: {
89
+ readonly contextSequence: unknown;
90
+ readonly systemPrompt?: string;
91
+ }) => LlamaChatSessionPeer;
92
+ //#endregion
93
+ export { LlamaCppNodeRuntimeOverrides, LlamaInstance, LlamaModelInstance, LlamaSessionInstance };
94
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","names":[],"sources":["../src/runtime.ts"],"sourcesContent":[],"mappings":";;AAaA;AASA;;;;;;AAeA;AAgBA;;;AASW,UAjDM,aAAA,CAiDN;EAEI,SAAA,CAAA,IAAA,EAAA;IAAR,SAAA,EAAA,MAAA;IAMuB,SAAA,CAAA,EAAA,MAAA,GAAA,MAAA;EAAoB,CAAA,CAAA,EAxDqB,OAwDrB,CAxD6B,kBAwD7B,CAAA;AAWlD;AAaA;;;;;UAxEiB,kBAAA;;8CAE6B,cAAc;;;MACV;;;;;;cAIpC;;;;;;;UAQG,oBAAA;;sBAIO;;;MAInB;;;;;;;UAQY,4BAAA;;4BAEW,QAAQ;;;;;;mCAOzB,wCAEJ,QAAQ;;;;;;8BAMe;;;;;;;;;;UAWb,oBAAA;;sBAIO;;;;MAKnB;;;KAIO,oBAAA;;;MAGN"}
@@ -0,0 +1,47 @@
1
+ //#region src/runtime.ts
2
+ let cachedLlama = null;
3
+ let cachedChatSessionCtor = null;
4
+ /**
5
+ * Lazily load the `node-llama-cpp` peer and return the `Llama` engine
6
+ * instance. Cached per process.
7
+ *
8
+ * @internal
9
+ */
10
+ async function loadLlamaModule(overrides) {
11
+ if (overrides?.getLlama !== void 0) return overrides.getLlama();
12
+ if (cachedLlama !== null) return cachedLlama;
13
+ let mod;
14
+ try {
15
+ mod = await import("node-llama-cpp");
16
+ } catch (cause) {
17
+ throw new Error("[graphorin/provider-llamacpp-node] missing peer dependency 'node-llama-cpp'. Install it with `pnpm add node-llama-cpp`.", { cause });
18
+ }
19
+ if (typeof mod.getLlama !== "function") throw new Error("[graphorin/provider-llamacpp-node] installed node-llama-cpp does not expose getLlama().");
20
+ const instance = await mod.getLlama();
21
+ cachedLlama = instance;
22
+ return instance;
23
+ }
24
+ /**
25
+ * Lazily resolve the peer's `LlamaChatSession` constructor for the
26
+ * real default session factory (PS-3). Cached per process; the
27
+ * override wins for tests.
28
+ *
29
+ * @internal
30
+ */
31
+ async function loadLlamaChatSessionCtor(overrides) {
32
+ if (overrides?.LlamaChatSession !== void 0) return overrides.LlamaChatSession;
33
+ if (cachedChatSessionCtor !== null) return cachedChatSessionCtor;
34
+ let mod;
35
+ try {
36
+ mod = await import("node-llama-cpp");
37
+ } catch (cause) {
38
+ throw new Error("[graphorin/provider-llamacpp-node] missing peer dependency 'node-llama-cpp'. Install it with `pnpm add node-llama-cpp`.", { cause });
39
+ }
40
+ if (typeof mod.LlamaChatSession !== "function") throw new Error("[graphorin/provider-llamacpp-node] installed node-llama-cpp does not expose LlamaChatSession.");
41
+ cachedChatSessionCtor = mod.LlamaChatSession;
42
+ return cachedChatSessionCtor;
43
+ }
44
+
45
+ //#endregion
46
+ export { loadLlamaChatSessionCtor, loadLlamaModule };
47
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","names":["cachedLlama: LlamaInstance | null","cachedChatSessionCtor: LlamaChatSessionCtor | null","mod: { getLlama?: unknown }","mod: { LlamaChatSession?: unknown }"],"sources":["../src/runtime.ts"],"sourcesContent":["/**\n * Loose structural shapes covering the slice of `node-llama-cpp` we\n * use. Re-declared here to avoid pulling the heavy native peer at\n * type-check time.\n *\n * @internal\n */\n\n/**\n * `Llama` engine instance (returned by `getLlama()`).\n *\n * @internal\n */\nexport interface LlamaInstance {\n loadModel(args: { modelPath: string; gpuLayers?: number | 'auto' }): Promise<LlamaModelInstance>;\n}\n\n/**\n * Loaded GGUF model.\n *\n * @internal\n */\nexport interface LlamaModelInstance {\n readonly trainContextSize?: number;\n tokenize(text: string): readonly number[] | Uint32Array | Uint8Array;\n createContext(args?: { contextSize?: number }): Promise<{\n getSequence(): { dispose?: () => void };\n dispose?: () => void;\n }>;\n dispose?(): Promise<void>;\n}\n\n/**\n * Loaded chat session capable of streaming responses.\n *\n * @internal\n */\nexport interface LlamaSessionInstance {\n promptStreamingResponse(\n prompt: string,\n options?: {\n readonly signal?: AbortSignal;\n readonly maxTokens?: number;\n readonly temperature?: number;\n },\n ): AsyncIterable<string>;\n}\n\n/**\n * Test-only shape for injecting fixture-driven runtime behaviour.\n *\n * @stable\n */\nexport interface LlamaCppNodeRuntimeOverrides {\n /** Returns a `LlamaInstance` (the result of `getLlama()`). */\n readonly getLlama?: () => Promise<LlamaInstance>;\n /**\n * Build a streaming chat session against an already-loaded model\n * instance. Used by the adapter to wire `model.tokenize` and\n * `session.promptStreamingResponse` to the per-test fixture.\n */\n readonly createSession?: (\n model: LlamaModelInstance,\n system?: string,\n ) => Promise<LlamaSessionInstance>;\n /**\n * Override the `LlamaChatSession` constructor used by the REAL\n * default session factory (PS-3). Tests stub it; production loads it\n * from the `node-llama-cpp` peer.\n */\n readonly LlamaChatSession?: LlamaChatSessionCtor;\n}\n\n/**\n * Structural slice of the peer's `LlamaChatSession` class used by the\n * default session factory (PS-3): `prompt(text, { onTextChunk })`\n * resolves with the full response while streaming chunks through the\n * callback.\n *\n * @internal\n */\nexport interface LlamaChatSessionPeer {\n prompt(\n text: string,\n options?: {\n readonly signal?: AbortSignal;\n readonly maxTokens?: number;\n readonly temperature?: number;\n readonly onTextChunk?: (chunk: string) => void;\n },\n ): Promise<string>;\n}\n\n/** @internal */\nexport type LlamaChatSessionCtor = new (args: {\n readonly contextSequence: unknown;\n readonly systemPrompt?: string;\n}) => LlamaChatSessionPeer;\n\nlet cachedLlama: LlamaInstance | null = null;\nlet cachedChatSessionCtor: LlamaChatSessionCtor | null = null;\n\n/**\n * Lazily load the `node-llama-cpp` peer and return the `Llama` engine\n * instance. Cached per process.\n *\n * @internal\n */\nexport async function loadLlamaModule(\n overrides: LlamaCppNodeRuntimeOverrides | undefined,\n): Promise<LlamaInstance> {\n if (overrides?.getLlama !== undefined) return overrides.getLlama();\n if (cachedLlama !== null) return cachedLlama;\n let mod: { getLlama?: unknown };\n try {\n mod = (await import('node-llama-cpp')) as { getLlama?: unknown };\n } catch (cause) {\n throw new Error(\n \"[graphorin/provider-llamacpp-node] missing peer dependency 'node-llama-cpp'. \" +\n 'Install it with `pnpm add node-llama-cpp`.',\n { cause },\n );\n }\n if (typeof mod.getLlama !== 'function') {\n throw new Error(\n '[graphorin/provider-llamacpp-node] installed node-llama-cpp does not expose getLlama().',\n );\n }\n const instance = (await (mod.getLlama as () => Promise<LlamaInstance>)()) as LlamaInstance;\n cachedLlama = instance;\n return instance;\n}\n\n/**\n * Lazily resolve the peer's `LlamaChatSession` constructor for the\n * real default session factory (PS-3). Cached per process; the\n * override wins for tests.\n *\n * @internal\n */\nexport async function loadLlamaChatSessionCtor(\n overrides: LlamaCppNodeRuntimeOverrides | undefined,\n): Promise<LlamaChatSessionCtor> {\n if (overrides?.LlamaChatSession !== undefined) return overrides.LlamaChatSession;\n if (cachedChatSessionCtor !== null) return cachedChatSessionCtor;\n let mod: { LlamaChatSession?: unknown };\n try {\n mod = (await import('node-llama-cpp')) as { LlamaChatSession?: unknown };\n } catch (cause) {\n throw new Error(\n \"[graphorin/provider-llamacpp-node] missing peer dependency 'node-llama-cpp'. \" +\n 'Install it with `pnpm add node-llama-cpp`.',\n { cause },\n );\n }\n if (typeof mod.LlamaChatSession !== 'function') {\n throw new Error(\n '[graphorin/provider-llamacpp-node] installed node-llama-cpp does not expose LlamaChatSession.',\n );\n }\n cachedChatSessionCtor = mod.LlamaChatSession as LlamaChatSessionCtor;\n return cachedChatSessionCtor;\n}\n\n/**\n * Test-only hook that resets the cached `getLlama()` result.\n *\n * @internal\n */\nexport function __resetLlamaCache(): void {\n cachedLlama = null;\n cachedChatSessionCtor = null;\n}\n"],"mappings":";AAmGA,IAAIA,cAAoC;AACxC,IAAIC,wBAAqD;;;;;;;AAQzD,eAAsB,gBACpB,WACwB;AACxB,KAAI,WAAW,aAAa,OAAW,QAAO,UAAU,UAAU;AAClE,KAAI,gBAAgB,KAAM,QAAO;CACjC,IAAIC;AACJ,KAAI;AACF,QAAO,MAAM,OAAO;UACb,OAAO;AACd,QAAM,IAAI,MACR,2HAEA,EAAE,OAAO,CACV;;AAEH,KAAI,OAAO,IAAI,aAAa,WAC1B,OAAM,IAAI,MACR,0FACD;CAEH,MAAM,WAAY,MAAO,IAAI,UAA2C;AACxE,eAAc;AACd,QAAO;;;;;;;;;AAUT,eAAsB,yBACpB,WAC+B;AAC/B,KAAI,WAAW,qBAAqB,OAAW,QAAO,UAAU;AAChE,KAAI,0BAA0B,KAAM,QAAO;CAC3C,IAAIC;AACJ,KAAI;AACF,QAAO,MAAM,OAAO;UACb,OAAO;AACd,QAAM,IAAI,MACR,2HAEA,EAAE,OAAO,CACV;;AAEH,KAAI,OAAO,IAAI,qBAAqB,WAClC,OAAM,IAAI,MACR,gGACD;AAEH,yBAAwB,IAAI;AAC5B,QAAO"}
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@graphorin/provider-llamacpp-node",
3
+ "version": "0.5.0",
4
+ "description": "In-process GGUF execution adapter for the Graphorin framework. Wraps node-llama-cpp@^3.5 to load .gguf model files directly into the same Node process — no daemon, no port to manage, no GPU contention with other processes. Declares trust: 'loopback' permanently because the model lives in the same trust boundary as the host process. Ships LlamaCppNativeCounter that wraps model.tokenize() from the loaded GGUF for byte-exact token counts.",
5
+ "license": "MIT",
6
+ "author": "Oleksiy Stepurenko",
7
+ "homepage": "https://github.com/o-stepper/graphorin/tree/main/packages/provider-llamacpp-node",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/o-stepper/graphorin.git",
11
+ "directory": "packages/provider-llamacpp-node"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/o-stepper/graphorin/issues"
15
+ },
16
+ "keywords": [
17
+ "graphorin",
18
+ "ai",
19
+ "agents",
20
+ "framework",
21
+ "provider",
22
+ "llm",
23
+ "llama-cpp",
24
+ "node-llama-cpp",
25
+ "gguf",
26
+ "in-process",
27
+ "local-first",
28
+ "loopback"
29
+ ],
30
+ "type": "module",
31
+ "engines": {
32
+ "node": ">=22.0.0"
33
+ },
34
+ "main": "./dist/index.js",
35
+ "module": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "exports": {
38
+ ".": {
39
+ "types": "./dist/index.d.ts",
40
+ "import": "./dist/index.js"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "README.md",
47
+ "CHANGELOG.md",
48
+ "LICENSE"
49
+ ],
50
+ "dependencies": {
51
+ "@graphorin/core": "0.5.0",
52
+ "@graphorin/provider": "0.5.0"
53
+ },
54
+ "peerDependencies": {
55
+ "node-llama-cpp": "^3.5.0"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "node-llama-cpp": {
59
+ "optional": false
60
+ }
61
+ },
62
+ "publishConfig": {
63
+ "access": "public",
64
+ "provenance": true
65
+ },
66
+ "devDependencies": {},
67
+ "scripts": {
68
+ "build": "tsdown",
69
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
70
+ "test": "vitest run",
71
+ "lint": "biome check .",
72
+ "clean": "rimraf dist .turbo *.tsbuildinfo"
73
+ }
74
+ }