@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.
- package/LICENSE +105 -0
- package/README.md +232 -0
- package/dist/bid-responder-Q725ZIUC.js +86 -0
- package/dist/bootstrap.js +22 -0
- package/dist/chain-info-lightweight-2UWAQZBF.js +303 -0
- package/dist/chat-stream-handler-BSHSGMFF.js +127 -0
- package/dist/chunk-2X7MSWD4.js +270 -0
- package/dist/chunk-3BHRQWSM.js +531 -0
- package/dist/chunk-5QFTU52A.js +442 -0
- package/dist/chunk-5ZAJBIAV.js +25 -0
- package/dist/chunk-7FLDR5NT.js +186 -0
- package/dist/chunk-C5XRYLYP.js +137 -0
- package/dist/chunk-D7ADMHK2.js +36 -0
- package/dist/chunk-DXUYWRO7.js +23 -0
- package/dist/chunk-F5UDK56Z.js +289 -0
- package/dist/chunk-NEHR6XY7.js +111 -0
- package/dist/chunk-NMJVODKH.js +453 -0
- package/dist/chunk-PRVT22SM.js +324 -0
- package/dist/chunk-T2ZRG5CX.js +1380 -0
- package/dist/chunk-V2L5SXTL.js +88 -0
- package/dist/chunk-XL2NJWFY.js +702 -0
- package/dist/embedding-C6GE3WVM.js +16 -0
- package/dist/hardware-ITQQJ5YI.js +37 -0
- package/dist/index.js +16836 -0
- package/dist/inference-server-CIGRJ36H.js +25 -0
- package/dist/local-cors-J6RWNMMD.js +44 -0
- package/dist/model-catalog-C53SDFMG.js +15 -0
- package/dist/model-discovery-LA6YMT3I.js +10 -0
- package/dist/ollama-XVXA3A37.js +9 -0
- package/dist/rewards-vault-cli-HW7H4EMD.js +147 -0
- package/dist/scripts/create_nodes.sh +6 -0
- package/dist/scripts/diloco_train.py +319 -0
- package/dist/scripts/train_lora.py +237 -0
- package/dist/scripts/train_micro.py +586 -0
- package/dist/trainer-HQMV2ZAR.js +21 -0
- package/package.json +128 -0
- package/scripts/create_nodes.sh +6 -0
- package/scripts/diloco_train.py +319 -0
- package/scripts/train_lora.py +237 -0
- 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
|
+
};
|