@synapseia-network/node 0.8.5

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.
Files changed (40) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +232 -0
  3. package/dist/bid-responder-Q725ZIUC.js +86 -0
  4. package/dist/bootstrap.js +22 -0
  5. package/dist/chain-info-lightweight-2UWAQZBF.js +303 -0
  6. package/dist/chat-stream-handler-BSHSGMFF.js +127 -0
  7. package/dist/chunk-2X7MSWD4.js +270 -0
  8. package/dist/chunk-3BHRQWSM.js +531 -0
  9. package/dist/chunk-5QFTU52A.js +442 -0
  10. package/dist/chunk-5ZAJBIAV.js +25 -0
  11. package/dist/chunk-7FLDR5NT.js +186 -0
  12. package/dist/chunk-C5XRYLYP.js +137 -0
  13. package/dist/chunk-D7ADMHK2.js +36 -0
  14. package/dist/chunk-DXUYWRO7.js +23 -0
  15. package/dist/chunk-F5UDK56Z.js +289 -0
  16. package/dist/chunk-NEHR6XY7.js +111 -0
  17. package/dist/chunk-NMJVODKH.js +453 -0
  18. package/dist/chunk-PRVT22SM.js +324 -0
  19. package/dist/chunk-T2ZRG5CX.js +1380 -0
  20. package/dist/chunk-V2L5SXTL.js +88 -0
  21. package/dist/chunk-XL2NJWFY.js +702 -0
  22. package/dist/embedding-C6GE3WVM.js +16 -0
  23. package/dist/hardware-ITQQJ5YI.js +37 -0
  24. package/dist/index.js +16836 -0
  25. package/dist/inference-server-CIGRJ36H.js +25 -0
  26. package/dist/local-cors-J6RWNMMD.js +44 -0
  27. package/dist/model-catalog-C53SDFMG.js +15 -0
  28. package/dist/model-discovery-LA6YMT3I.js +10 -0
  29. package/dist/ollama-XVXA3A37.js +9 -0
  30. package/dist/rewards-vault-cli-HW7H4EMD.js +147 -0
  31. package/dist/scripts/create_nodes.sh +6 -0
  32. package/dist/scripts/diloco_train.py +319 -0
  33. package/dist/scripts/train_lora.py +237 -0
  34. package/dist/scripts/train_micro.py +586 -0
  35. package/dist/trainer-HQMV2ZAR.js +21 -0
  36. package/package.json +128 -0
  37. package/scripts/create_nodes.sh +6 -0
  38. package/scripts/diloco_train.py +319 -0
  39. package/scripts/train_lora.py +237 -0
  40. package/scripts/train_micro.py +586 -0
@@ -0,0 +1,303 @@
1
+ import { fileURLToPath as __synFup } from "url";import { dirname as __synDn } from "path";const __filename = __synFup(import.meta.url);const __dirname = __synDn(__filename);
2
+ import {
3
+ getCoordinatorUrl
4
+ } from "./chunk-DXUYWRO7.js";
5
+ import {
6
+ __name
7
+ } from "./chunk-D7ADMHK2.js";
8
+
9
+ // src/cli/chain-info-lightweight.ts
10
+ import { readFileSync, existsSync } from "fs";
11
+ import { join } from "path";
12
+ import { homedir } from "os";
13
+ function walletDir() {
14
+ return process.env.SYNAPSEIA_HOME ?? join(homedir(), ".synapseia");
15
+ }
16
+ __name(walletDir, "walletDir");
17
+ function readPublicKey() {
18
+ const walletPath = join(walletDir(), "wallet.json");
19
+ if (!existsSync(walletPath)) return null;
20
+ try {
21
+ const raw = JSON.parse(readFileSync(walletPath, "utf-8"));
22
+ if (typeof raw.publicKey === "string" && raw.publicKey.length >= 32) {
23
+ return raw.publicKey;
24
+ }
25
+ return null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+ __name(readPublicKey, "readPublicKey");
31
+ function readCoordinatorUrl() {
32
+ return getCoordinatorUrl();
33
+ }
34
+ __name(readCoordinatorUrl, "readCoordinatorUrl");
35
+ async function fetchSolBalance(rpcUrl, pubkey) {
36
+ const { Connection, PublicKey } = await import("@solana/web3.js");
37
+ const conn = new Connection(rpcUrl, "confirmed");
38
+ try {
39
+ const lamports = await conn.getBalance(new PublicKey(pubkey));
40
+ return lamports / 1e9;
41
+ } catch {
42
+ return 0;
43
+ }
44
+ }
45
+ __name(fetchSolBalance, "fetchSolBalance");
46
+ async function fetchSynBalance(rpcUrl, pubkey, mint) {
47
+ const { Connection, PublicKey } = await import("@solana/web3.js");
48
+ const conn = new Connection(rpcUrl, "confirmed");
49
+ try {
50
+ const accounts = await conn.getTokenAccountsByOwner(new PublicKey(pubkey), {
51
+ mint: new PublicKey(mint)
52
+ });
53
+ if (accounts.value.length === 0) {
54
+ return {
55
+ amount: 0,
56
+ accountExists: false
57
+ };
58
+ }
59
+ let total = 0n;
60
+ for (const acc of accounts.value) {
61
+ const info = await conn.getTokenAccountBalance(acc.pubkey);
62
+ total += BigInt(info.value.amount);
63
+ }
64
+ return {
65
+ amount: Number(total) / 1e9,
66
+ accountExists: true
67
+ };
68
+ } catch {
69
+ return {
70
+ amount: 0,
71
+ accountExists: false
72
+ };
73
+ }
74
+ }
75
+ __name(fetchSynBalance, "fetchSynBalance");
76
+ async function fetchStakeInfo(rpcUrl, ownerPubkey, stakingProgramId) {
77
+ try {
78
+ const { Connection, PublicKey } = await import("@solana/web3.js");
79
+ const conn = new Connection(rpcUrl, "confirmed");
80
+ const owner = new PublicKey(ownerPubkey);
81
+ const accounts = await conn.getProgramAccounts(new PublicKey(stakingProgramId), {
82
+ filters: [
83
+ {
84
+ memcmp: {
85
+ offset: 8,
86
+ bytes: owner.toBase58()
87
+ }
88
+ }
89
+ ]
90
+ });
91
+ if (accounts.length === 0) {
92
+ return {
93
+ exists: false,
94
+ amount: 0,
95
+ rewardsPending: 0,
96
+ lockedUntil: 0
97
+ };
98
+ }
99
+ const data = accounts[0].account.data;
100
+ if (data.length < 178) {
101
+ return {
102
+ exists: false,
103
+ amount: 0,
104
+ rewardsPending: 0,
105
+ lockedUntil: 0
106
+ };
107
+ }
108
+ const amount = Number(data.readBigUInt64LE(72)) / 1e9;
109
+ const lockedUntil = Number(data.readBigInt64LE(162));
110
+ const rewardsPending = Number(data.readBigUInt64LE(170)) / 1e9;
111
+ return {
112
+ exists: true,
113
+ amount,
114
+ rewardsPending,
115
+ lockedUntil
116
+ };
117
+ } catch {
118
+ return {
119
+ exists: false,
120
+ amount: 0,
121
+ rewardsPending: 0,
122
+ lockedUntil: 0
123
+ };
124
+ }
125
+ }
126
+ __name(fetchStakeInfo, "fetchStakeInfo");
127
+ async function fetchCoordinatorReachable(coordinatorUrl) {
128
+ try {
129
+ const res = await fetch(`${coordinatorUrl}/health`, {
130
+ signal: AbortSignal.timeout(3e3)
131
+ });
132
+ return res.ok;
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+ __name(fetchCoordinatorReachable, "fetchCoordinatorReachable");
138
+ async function fetchVaultClaimable(rpcUrl, ownerPubkey, vaultProgramId) {
139
+ try {
140
+ const { Connection, PublicKey } = await import("@solana/web3.js");
141
+ const conn = new Connection(rpcUrl, "confirmed");
142
+ const owner = new PublicKey(ownerPubkey);
143
+ const program = new PublicKey(vaultProgramId);
144
+ const [pda] = PublicKey.findProgramAddressSync([
145
+ Buffer.from("reward_account"),
146
+ owner.toBuffer()
147
+ ], program);
148
+ const info = await conn.getAccountInfo(pda, "confirmed");
149
+ if (!info || info.data.length < 8 + 32 + 8) return 0;
150
+ const lamports = info.data.readBigUInt64LE(8 + 32);
151
+ return Number(lamports) / 1e9;
152
+ } catch {
153
+ return 0;
154
+ }
155
+ }
156
+ __name(fetchVaultClaimable, "fetchVaultClaimable");
157
+ async function fetchRewardsBreakdown(coordinatorUrl, pubkey) {
158
+ try {
159
+ const res = await fetch(`${coordinatorUrl}/rewards/claimable/${encodeURIComponent(pubkey)}`, {
160
+ signal: AbortSignal.timeout(3e3)
161
+ });
162
+ if (!res.ok) return {
163
+ byType: {},
164
+ totalClaimedSyn: 0
165
+ };
166
+ const data = await res.json();
167
+ const byType = {};
168
+ for (const [k, v] of Object.entries(data.byType ?? {})) {
169
+ const n = typeof v === "string" ? parseFloat(v) : v;
170
+ if (Number.isFinite(n)) byType[k] = n;
171
+ }
172
+ const totalClaimedSyn = typeof data.totalClaimedSyn === "string" ? parseFloat(data.totalClaimedSyn) : typeof data.totalClaimedSyn === "number" ? data.totalClaimedSyn : 0;
173
+ return {
174
+ byType,
175
+ totalClaimedSyn: Number.isFinite(totalClaimedSyn) ? totalClaimedSyn : 0
176
+ };
177
+ } catch {
178
+ return {
179
+ byType: {},
180
+ totalClaimedSyn: 0
181
+ };
182
+ }
183
+ }
184
+ __name(fetchRewardsBreakdown, "fetchRewardsBreakdown");
185
+ var EMPTY_STATS = {
186
+ presencePoints: 0,
187
+ totalWins: 0,
188
+ totalSubmissions: 0,
189
+ unclaimedSyn: 0,
190
+ totalClaimedSyn: 0,
191
+ canaryStrikes: 0,
192
+ anomalyWarnings: 0,
193
+ attestationFailures: 0,
194
+ tier: null,
195
+ name: null
196
+ };
197
+ async function fetchNodeStats(coordinatorUrl, pubkey) {
198
+ try {
199
+ const res = await fetch(`${coordinatorUrl}/node/${encodeURIComponent(pubkey)}`, {
200
+ signal: AbortSignal.timeout(3e3)
201
+ });
202
+ if (!res.ok) return EMPTY_STATS;
203
+ const data = await res.json();
204
+ const toNumber = /* @__PURE__ */ __name((v) => {
205
+ const n = typeof v === "string" ? parseFloat(v) : typeof v === "number" ? v : 0;
206
+ return Number.isFinite(n) ? n : 0;
207
+ }, "toNumber");
208
+ const tierRaw = data.tier;
209
+ const tierNum = typeof tierRaw === "string" ? parseInt(tierRaw, 10) : typeof tierRaw === "number" ? tierRaw : NaN;
210
+ return {
211
+ presencePoints: toNumber(data.presencePoints),
212
+ totalWins: toNumber(data.totalWins),
213
+ totalSubmissions: toNumber(data.totalSubmissions),
214
+ unclaimedSyn: toNumber(data.unclaimedSyn),
215
+ totalClaimedSyn: toNumber(data.totalClaimedSyn),
216
+ canaryStrikes: toNumber(data.canaryStrikes),
217
+ anomalyWarnings: toNumber(data.anomalyWarnings),
218
+ attestationFailures: toNumber(data.attestationFailures),
219
+ tier: Number.isFinite(tierNum) ? tierNum : null,
220
+ name: typeof data.name === "string" ? data.name : null
221
+ };
222
+ } catch {
223
+ return EMPTY_STATS;
224
+ }
225
+ }
226
+ __name(fetchNodeStats, "fetchNodeStats");
227
+ async function runChainInfoLightweight() {
228
+ const wallet = readPublicKey();
229
+ const rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.devnet.solana.com";
230
+ const synMint = process.env.SYN_TOKEN_MINT ?? "DCdWHhoeEwHJ3Fy3DRTk4yvZPXq3mSNZKtbPJzUfpUh8";
231
+ const stakingProgramId = process.env.STAKING_PROGRAM_ID ?? "CYW5Cprp5JuzaXtPyV8LPBgPzbze6QHnc3oFBAVaFkfw";
232
+ const rewardsVaultProgramId = process.env.REWARDS_VAULT_PROGRAM_ID ?? "D9pkzWv2Ak9J8vXDVcMM1P51hDmjRJEwbuYHxCuJKTEN";
233
+ const emptyPayload = {
234
+ wallet,
235
+ sol: 0,
236
+ syn: 0,
237
+ staked: 0,
238
+ rewardsPending: 0,
239
+ stakeAccountExists: false,
240
+ stakeLockedUntil: 0,
241
+ tokenAccountExists: false,
242
+ coordinatorReachable: false,
243
+ vaultClaimableSyn: 0,
244
+ rewardsByType: {},
245
+ presencePoints: 0,
246
+ totalWins: 0,
247
+ totalSubmissions: 0,
248
+ unclaimedSyn: 0,
249
+ totalClaimedSyn: 0,
250
+ canaryStrikes: 0,
251
+ anomalyWarnings: 0,
252
+ attestationFailures: 0,
253
+ tier: null,
254
+ nodeName: null
255
+ };
256
+ if (!wallet) {
257
+ process.stdout.write(`__CHAIN_INFO__ ${JSON.stringify(emptyPayload)}
258
+ `);
259
+ process.exit(0);
260
+ }
261
+ const coordinatorUrl = readCoordinatorUrl();
262
+ const [solBalance, syn, stake, stats, coordReachable, vaultClaimable, breakdown] = await Promise.all([
263
+ fetchSolBalance(rpcUrl, wallet),
264
+ fetchSynBalance(rpcUrl, wallet, synMint),
265
+ fetchStakeInfo(rpcUrl, wallet, stakingProgramId),
266
+ fetchNodeStats(coordinatorUrl, wallet),
267
+ fetchCoordinatorReachable(coordinatorUrl),
268
+ fetchVaultClaimable(rpcUrl, wallet, rewardsVaultProgramId),
269
+ fetchRewardsBreakdown(coordinatorUrl, wallet)
270
+ ]);
271
+ const payload = {
272
+ ...emptyPayload,
273
+ sol: solBalance,
274
+ syn: syn.amount,
275
+ tokenAccountExists: syn.accountExists,
276
+ staked: stake.amount,
277
+ rewardsPending: stake.rewardsPending,
278
+ stakeAccountExists: stake.exists,
279
+ stakeLockedUntil: stake.lockedUntil,
280
+ coordinatorReachable: coordReachable,
281
+ vaultClaimableSyn: vaultClaimable,
282
+ rewardsByType: breakdown.byType,
283
+ presencePoints: stats.presencePoints,
284
+ totalWins: stats.totalWins,
285
+ totalSubmissions: stats.totalSubmissions,
286
+ unclaimedSyn: stats.unclaimedSyn,
287
+ // Prefer the /rewards/claimable endpoint's totalClaimedSyn over the one
288
+ // from /node/:wallet when available — it's the authoritative count.
289
+ totalClaimedSyn: breakdown.totalClaimedSyn || stats.totalClaimedSyn,
290
+ canaryStrikes: stats.canaryStrikes,
291
+ anomalyWarnings: stats.anomalyWarnings,
292
+ attestationFailures: stats.attestationFailures,
293
+ tier: stats.tier,
294
+ nodeName: stats.name
295
+ };
296
+ process.stdout.write(`__CHAIN_INFO__ ${JSON.stringify(payload)}
297
+ `);
298
+ process.exit(0);
299
+ }
300
+ __name(runChainInfoLightweight, "runChainInfoLightweight");
301
+ export {
302
+ runChainInfoLightweight
303
+ };
@@ -0,0 +1,127 @@
1
+ import { fileURLToPath as __synFup } from "url";import { dirname as __synDn } from "path";const __filename = __synFup(import.meta.url);const __dirname = __synDn(__filename);
2
+ import {
3
+ LlmProviderHelper,
4
+ readJsonFromStream,
5
+ sendJsonOverStream
6
+ } from "./chunk-T2ZRG5CX.js";
7
+ import {
8
+ beginChatInference,
9
+ endChatInference
10
+ } from "./chunk-5ZAJBIAV.js";
11
+ import {
12
+ CHAT_PROTOCOL
13
+ } from "./chunk-3BHRQWSM.js";
14
+ import "./chunk-2X7MSWD4.js";
15
+ import {
16
+ init_logger,
17
+ logger_default
18
+ } from "./chunk-V2L5SXTL.js";
19
+ import {
20
+ __name
21
+ } from "./chunk-D7ADMHK2.js";
22
+
23
+ // src/modules/inference/chat-stream-handler.ts
24
+ init_logger();
25
+ var ChatStreamHandler = class {
26
+ static {
27
+ __name(this, "ChatStreamHandler");
28
+ }
29
+ p2p;
30
+ deps;
31
+ llmProvider = new LlmProviderHelper();
32
+ constructor(p2p, deps) {
33
+ this.p2p = p2p;
34
+ this.deps = deps;
35
+ }
36
+ async start() {
37
+ await this.p2p.handleProtocol(CHAT_PROTOCOL, (stream, _connection) => {
38
+ void this.onStream(stream);
39
+ });
40
+ const modelTag = this.deps.llmModel.provider === "cloud" ? `${this.deps.llmModel.providerId}/${this.deps.llmModel.modelId}` : this.deps.llmModel.modelId;
41
+ logger_default.log(`[ChatStreamHandler] listening on ${CHAT_PROTOCOL} (llm=${modelTag})`);
42
+ }
43
+ async onStream(stream) {
44
+ logger_default.log(`[ChatStreamHandler] \u26A1 inbound stream opened \u2014 reading request\u2026`);
45
+ try {
46
+ const req = await readJsonFromStream(stream);
47
+ if (!req?.messages || !Array.isArray(req.messages)) {
48
+ await sendJsonOverStream(stream, {
49
+ choices: [
50
+ {
51
+ message: {
52
+ role: "assistant",
53
+ content: "invalid request"
54
+ }
55
+ }
56
+ ]
57
+ });
58
+ return;
59
+ }
60
+ logger_default.log(`[ChatStreamHandler] \u25B6 quote ${req.quoteId?.slice(0, 8)}\u2026 session ${req.sessionId?.slice(0, 8)}\u2026 (${req.messages.length} messages) \u2014 generating via ${this.deps.llmModel.provider}`);
61
+ beginChatInference();
62
+ const t0 = Date.now();
63
+ let response;
64
+ try {
65
+ response = await this.generateAnswer(req.messages);
66
+ } finally {
67
+ endChatInference();
68
+ }
69
+ logger_default.log(`[ChatStreamHandler] \u2713 LLM responded in ${Date.now() - t0}ms \u2014 writing response`);
70
+ await sendJsonOverStream(stream, response);
71
+ logger_default.log(`[ChatStreamHandler] \u2713 response sent for quote ${req.quoteId?.slice(0, 8)}\u2026`);
72
+ } catch (err) {
73
+ logger_default.warn(`[ChatStreamHandler] stream error: ${err.message}`);
74
+ try {
75
+ await sendJsonOverStream(stream, {
76
+ choices: [
77
+ {
78
+ message: {
79
+ role: "assistant",
80
+ content: `error: ${err.message}`
81
+ }
82
+ }
83
+ ]
84
+ });
85
+ } catch {
86
+ }
87
+ }
88
+ }
89
+ /**
90
+ * Flatten chat messages into a single prompt that preserves role context
91
+ * (the LlmProviderHelper.generateLLM API is prompt-based, not
92
+ * messages-based). This keeps the implementation simple — a future
93
+ * iteration can add a chat-native path through the cloud providers'
94
+ * `/chat/completions` endpoints to preserve tool-use / fine control.
95
+ */
96
+ async generateAnswer(messages) {
97
+ const prompt = this.flattenMessagesToPrompt(messages);
98
+ const content = await this.llmProvider.generateLLM(this.deps.llmModel, prompt, this.deps.llmConfig, {
99
+ temperature: 0.7,
100
+ maxTokens: 2048
101
+ });
102
+ return {
103
+ choices: [
104
+ {
105
+ message: {
106
+ role: "assistant",
107
+ content
108
+ }
109
+ }
110
+ ]
111
+ };
112
+ }
113
+ flattenMessagesToPrompt(messages) {
114
+ const parts = [
115
+ "You are Synapseia-Agent, a biomedical research assistant. Answer the user concisely and truthfully. If you do not know, say so."
116
+ ];
117
+ for (const m of messages) {
118
+ const role = m.role === "assistant" ? "Assistant" : m.role === "system" ? "System" : "User";
119
+ parts.push(`${role}: ${m.content}`);
120
+ }
121
+ parts.push("Assistant:");
122
+ return parts.join("\n\n");
123
+ }
124
+ };
125
+ export {
126
+ ChatStreamHandler
127
+ };
@@ -0,0 +1,270 @@
1
+ import { fileURLToPath as __synFup } from "url";import { dirname as __synDn } from "path";const __filename = __synFup(import.meta.url);const __dirname = __synDn(__filename);
2
+ import {
3
+ init_logger,
4
+ logger_default
5
+ } from "./chunk-V2L5SXTL.js";
6
+ import {
7
+ __name
8
+ } from "./chunk-D7ADMHK2.js";
9
+
10
+ // src/modules/llm/ollama.ts
11
+ import { Injectable, Logger } from "@nestjs/common";
12
+ import axios from "axios";
13
+ import { Ollama } from "ollama";
14
+
15
+ // src/utils/circuit-breaker.ts
16
+ init_logger();
17
+ var CircuitOpenError = class extends Error {
18
+ static {
19
+ __name(this, "CircuitOpenError");
20
+ }
21
+ retryAfterMs;
22
+ constructor(name, retryAfterMs) {
23
+ super(`circuit "${name}" open \u2014 retry in ${Math.round(retryAfterMs / 1e3)}s`), this.retryAfterMs = retryAfterMs;
24
+ this.name = "CircuitOpenError";
25
+ }
26
+ };
27
+ var CircuitBreaker = class {
28
+ static {
29
+ __name(this, "CircuitBreaker");
30
+ }
31
+ opts;
32
+ state = "closed";
33
+ failures = [];
34
+ openedAt = 0;
35
+ constructor(opts) {
36
+ this.opts = opts;
37
+ }
38
+ /**
39
+ * Run `op` under breaker control. Throws `CircuitOpenError` immediately
40
+ * when open; otherwise propagates the underlying success/failure.
41
+ */
42
+ async exec(op) {
43
+ const now = Date.now();
44
+ if (this.state === "open") {
45
+ const elapsed = now - this.openedAt;
46
+ if (elapsed < this.opts.cooldownMs) {
47
+ throw new CircuitOpenError(this.opts.name, this.opts.cooldownMs - elapsed);
48
+ }
49
+ this.state = "half-open";
50
+ }
51
+ try {
52
+ const result = await op();
53
+ this.onSuccess();
54
+ return result;
55
+ } catch (err) {
56
+ this.onFailure(now);
57
+ throw err;
58
+ }
59
+ }
60
+ onSuccess() {
61
+ if (this.state !== "closed") {
62
+ logger_default.warn(`[circuit:${this.opts.name}] recovered \u2014 closing`);
63
+ }
64
+ this.state = "closed";
65
+ this.failures = [];
66
+ }
67
+ onFailure(now) {
68
+ if (this.state === "half-open") {
69
+ this.openedAt = now;
70
+ this.state = "open";
71
+ logger_default.warn(`[circuit:${this.opts.name}] probe failed \u2014 reopening for ${this.opts.cooldownMs}ms`);
72
+ return;
73
+ }
74
+ const cutoff = now - this.opts.windowMs;
75
+ this.failures = this.failures.filter((t) => t >= cutoff);
76
+ this.failures.push(now);
77
+ if (this.failures.length >= this.opts.failureThreshold) {
78
+ this.openedAt = now;
79
+ this.state = "open";
80
+ this.failures = [];
81
+ logger_default.warn(`[circuit:${this.opts.name}] tripped \u2014 ${this.opts.failureThreshold} failures in ${this.opts.windowMs}ms; opening for ${this.opts.cooldownMs}ms`);
82
+ }
83
+ }
84
+ /** Test-only / introspection. */
85
+ getState() {
86
+ return this.state;
87
+ }
88
+ };
89
+
90
+ // src/modules/llm/ollama.ts
91
+ function _ts_decorate(decorators, target, key, desc) {
92
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
93
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
94
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
95
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
96
+ }
97
+ __name(_ts_decorate, "_ts_decorate");
98
+ var OllamaHelper = class _OllamaHelper {
99
+ static {
100
+ __name(this, "OllamaHelper");
101
+ }
102
+ /**
103
+ * Check Ollama availability and installed models
104
+ */
105
+ logger = new Logger(_OllamaHelper.name);
106
+ /**
107
+ * Per-process circuit breaker for `generate()`. When Ollama crashes
108
+ * under RAM pressure, the same generate calls fail in cascade. Without
109
+ * a breaker, every retry produces another `Generation failed: ...`
110
+ * log/telemetry event — 200+ events per minute observed in production.
111
+ *
112
+ * After 5 failures within 60s the breaker opens for 30s; subsequent
113
+ * calls throw CircuitOpenError immediately (no HTTP, no log spam) until
114
+ * the half-open probe succeeds.
115
+ */
116
+ static generateBreaker = new CircuitBreaker({
117
+ name: "ollama-generate",
118
+ failureThreshold: 5,
119
+ windowMs: 6e4,
120
+ cooldownMs: 3e4
121
+ });
122
+ async checkOllama(url = process.env.OLLAMA_URL || "http://localhost:11434") {
123
+ try {
124
+ const response = await axios.get(`${url}/api/tags`, {
125
+ timeout: 5e3
126
+ });
127
+ const models = response.data.models.map((m) => m.name);
128
+ const { HardwareHelper } = await import("./hardware-ITQQJ5YI.js");
129
+ const hwInfo = new HardwareHelper().detectHardware(false);
130
+ const hasGPU = hwInfo.gpuVramGb > 0;
131
+ const recommendedModel = hasGPU ? "qwen2.5:3b" : "qwen2.5:0.5b";
132
+ return {
133
+ available: true,
134
+ url,
135
+ models,
136
+ recommendedModel
137
+ };
138
+ } catch (error) {
139
+ const isAxiosError = error && typeof error === "object" && "isAxiosError" in error;
140
+ if (isAxiosError) {
141
+ return {
142
+ available: false,
143
+ url,
144
+ models: [],
145
+ recommendedModel: "qwen2.5:0.5b",
146
+ error: `Cannot connect to Ollama at ${url}: ${error.message}`
147
+ };
148
+ }
149
+ let errorMessage = "Unknown error";
150
+ if (error instanceof Error) errorMessage = error.message;
151
+ return {
152
+ available: false,
153
+ url,
154
+ models: [],
155
+ recommendedModel: "qwen2.5:0.5b",
156
+ error: errorMessage
157
+ };
158
+ }
159
+ }
160
+ /**
161
+ * Pull a model from Ollama registry
162
+ */
163
+ async pullModel(model, url = process.env.OLLAMA_URL || "http://localhost:11434") {
164
+ try {
165
+ this.logger.log(`\u{1F4E5} Pulling model ${model} from Ollama...`);
166
+ const ollamaClient = new Ollama({
167
+ host: url
168
+ });
169
+ const stream = await ollamaClient.pull({
170
+ model,
171
+ stream: true
172
+ });
173
+ let lastDigest = "";
174
+ for await (const part of stream) {
175
+ if (part.digest && part.digest !== lastDigest) {
176
+ const percent = part.total !== void 0 && part.completed !== void 0 ? Math.round(part.completed / part.total * 100) : 0;
177
+ this.logger.log(`\u{1F4E6} ${model}: ${percent}% complete`);
178
+ lastDigest = part.digest;
179
+ }
180
+ if (part.status === "success") {
181
+ this.logger.log(`\u2705 Model ${model} downloaded successfully`);
182
+ }
183
+ }
184
+ } catch (error) {
185
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
186
+ throw new Error(`Failed to pull model ${model}: ${errorMessage}`);
187
+ }
188
+ }
189
+ /**
190
+ * Generate text using Ollama
191
+ */
192
+ async generate(prompt, model, url = process.env.OLLAMA_URL || "http://localhost:11434", options) {
193
+ return _OllamaHelper.generateBreaker.exec(async () => {
194
+ try {
195
+ let targetModel = model;
196
+ if (!targetModel) {
197
+ const status = await this.checkOllama(url);
198
+ if (!status.available) throw new Error("Ollama is not available");
199
+ targetModel = status.recommendedModel;
200
+ }
201
+ this.logger.log(`\u{1F9E0} Generating with model: ${targetModel}`);
202
+ const ollamaClient = new Ollama({
203
+ host: url
204
+ });
205
+ const response = await ollamaClient.chat({
206
+ model: targetModel,
207
+ messages: [
208
+ {
209
+ role: "user",
210
+ content: prompt
211
+ }
212
+ ],
213
+ stream: false,
214
+ // format: "json" enables grammar-based constrained decoding — the model
215
+ // is physically prevented from emitting non-JSON tokens, so JSON.parse
216
+ // never fails due to syntax errors (no fences, no trailing text, no
217
+ // unclosed strings). Still need to validate fields after parsing.
218
+ ...options?.forceJson && {
219
+ format: "json"
220
+ },
221
+ options: {
222
+ // Raise context window above the Ollama default (2048) so research
223
+ // prompts carrying paper abstracts + related DOIs + ontology
224
+ // context don't get truncated — truncation was surfacing as
225
+ // "unexpected EOF" when the prompt end landed mid-token on the
226
+ // llama runner. Qwen2.5 supports 32k; 8192 is a safe fit for 0.5b
227
+ // on a 3.8 GiB Docker VM.
228
+ num_ctx: 8192,
229
+ ...options?.temperature !== void 0 && {
230
+ temperature: options.temperature
231
+ },
232
+ ...options?.maxTokens !== void 0 && {
233
+ num_predict: options.maxTokens
234
+ },
235
+ ...options?.topP !== void 0 && {
236
+ top_p: options.topP
237
+ }
238
+ }
239
+ });
240
+ return response.message.content.trim();
241
+ } catch (error) {
242
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
243
+ throw new Error(`Generation failed: ${errorMessage}`);
244
+ }
245
+ });
246
+ }
247
+ /**
248
+ * Ensure a model is available, pulling if necessary
249
+ */
250
+ async ensureModel(model, url = process.env.OLLAMA_URL || "http://localhost:11434") {
251
+ const status = await this.checkOllama(url);
252
+ if (!status.available) {
253
+ throw new Error("Ollama is not running. Start with: ollama serve");
254
+ }
255
+ const modelAvailable = status.models.some((m) => m.startsWith(model.split(":")[0]));
256
+ if (!modelAvailable) {
257
+ this.logger.log(`\u26A0\uFE0F Model ${model} not found. Pulling...`);
258
+ await this.pullModel(model, url);
259
+ } else {
260
+ this.logger.log(`\u2705 Model ${model} is available`);
261
+ }
262
+ }
263
+ };
264
+ OllamaHelper = _ts_decorate([
265
+ Injectable()
266
+ ], OllamaHelper);
267
+
268
+ export {
269
+ OllamaHelper
270
+ };