@orquentra/codebuddy 2.14.0 → 2.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +98 -0
- package/README.md +87 -6
- package/dist/{chunk-YNSTIMCF.js → chunk-4DTO2E4Y.js} +352 -157
- package/dist/chunk-4DTO2E4Y.js.map +1 -0
- package/dist/{chunk-CAEPHLHC.js → chunk-57642NGU.js} +2 -44
- package/dist/chunk-57642NGU.js.map +1 -0
- package/dist/chunk-6OQKL35B.js +55 -0
- package/dist/chunk-6OQKL35B.js.map +1 -0
- package/dist/{chunk-4DKYKDHI.js → chunk-7IPSDDO5.js} +5 -3
- package/dist/{chunk-4DKYKDHI.js.map → chunk-7IPSDDO5.js.map} +1 -1
- package/dist/chunk-7JPUMXFE.js +413 -0
- package/dist/chunk-7JPUMXFE.js.map +1 -0
- package/dist/{chunk-LRQNPYCK.js → chunk-AFXWCSQ4.js} +13 -7
- package/dist/chunk-AFXWCSQ4.js.map +1 -0
- package/dist/chunk-BNZDEU5K.js +368 -0
- package/dist/chunk-BNZDEU5K.js.map +1 -0
- package/dist/chunk-EMWGLKPR.js +45 -0
- package/dist/chunk-EMWGLKPR.js.map +1 -0
- package/dist/{chunk-YJPCS2GP.js → chunk-GNV4X67F.js} +59 -20
- package/dist/chunk-GNV4X67F.js.map +1 -0
- package/dist/{chunk-YWDH6DQ6.js → chunk-IM5RFEW4.js} +21 -19
- package/dist/{chunk-YWDH6DQ6.js.map → chunk-IM5RFEW4.js.map} +1 -1
- package/dist/chunk-K4XNTYV7.js +136 -0
- package/dist/chunk-K4XNTYV7.js.map +1 -0
- package/dist/{chunk-ESZXCZIN.js → chunk-MNSMW4MZ.js} +33 -5
- package/dist/chunk-MNSMW4MZ.js.map +1 -0
- package/dist/chunk-NNJSAYHY.js +372 -0
- package/dist/chunk-NNJSAYHY.js.map +1 -0
- package/dist/{chunk-XSNW6Y3X.js → chunk-OO7Q7XZF.js} +28 -24
- package/dist/chunk-OO7Q7XZF.js.map +1 -0
- package/dist/{chunk-L6BRYZ6G.js → chunk-PZ4EBFIA.js} +7 -7
- package/dist/{chunk-L6BRYZ6G.js.map → chunk-PZ4EBFIA.js.map} +1 -1
- package/dist/chunk-RGIVHKBH.js +156 -0
- package/dist/chunk-RGIVHKBH.js.map +1 -0
- package/dist/{chunk-LW75SKZR.js → chunk-RKF4HBBQ.js} +126 -18
- package/dist/chunk-RKF4HBBQ.js.map +1 -0
- package/dist/{chunk-7QMHIJBA.js → chunk-VBDM7HC5.js} +969 -968
- package/dist/chunk-VBDM7HC5.js.map +1 -0
- package/dist/chunk-VF7DKRIU.js +50 -0
- package/dist/chunk-VF7DKRIU.js.map +1 -0
- package/dist/{chunk-KNR2P2UY.js → chunk-Y4QQML24.js} +21 -6
- package/dist/chunk-Y4QQML24.js.map +1 -0
- package/dist/cli/index.js +4 -4
- package/dist/config-S2BDMT4B.js +15 -0
- package/dist/expand-TONFVT5D.js +98 -0
- package/dist/expand-TONFVT5D.js.map +1 -0
- package/dist/file-vectors-VREL75MA.js +19 -0
- package/dist/index-facts-MBJKBYA3.js +38 -0
- package/dist/index-pass-RP23G2CR.js +15 -0
- package/dist/index.js +24 -22
- package/dist/{install-EU35ERK5.js → install-MCJIJMFF.js} +11 -10
- package/dist/{lean-43ADYTBW.js → lean-CBHVW6JM.js} +8 -7
- package/dist/local-TH5XCLVJ.js +48 -0
- package/dist/local-llm-GZREB5DF.js +19 -0
- package/dist/local-llm-GZREB5DF.js.map +1 -0
- package/dist/{main-SFC5LV4E.js → main-QF6Z552J.js} +346 -140
- package/dist/main-QF6Z552J.js.map +1 -0
- package/dist/{memory-TGHVTBME.js → memory-LSZKSF53.js} +9 -6
- package/dist/{memory-TGHVTBME.js.map → memory-LSZKSF53.js.map} +1 -1
- package/dist/{operations-6Z6ZDVBP.js → operations-BPI633VN.js} +8 -7
- package/dist/operations-BPI633VN.js.map +1 -0
- package/dist/{rerank-IQAEWLQR.js → rerank-EPCTSEVP.js} +7 -4
- package/dist/{rerank-IQAEWLQR.js.map → rerank-EPCTSEVP.js.map} +1 -1
- package/dist/{runtime-W5AW32SD.js → runtime-W5ACTDFS.js} +9 -8
- package/dist/runtime-W5ACTDFS.js.map +1 -0
- package/dist/summaries-W2J5FCLG.js +32 -0
- package/dist/summaries-W2J5FCLG.js.map +1 -0
- package/dist/{zvec-24HLAFGN.js → zvec-LSA2IRAU.js} +13 -5
- package/dist/zvec-LSA2IRAU.js.map +1 -0
- package/package.json +3 -2
- package/dist/chunk-7QMHIJBA.js.map +0 -1
- package/dist/chunk-CAEPHLHC.js.map +0 -1
- package/dist/chunk-ESC4Y3L7.js +0 -204
- package/dist/chunk-ESC4Y3L7.js.map +0 -1
- package/dist/chunk-ESZXCZIN.js.map +0 -1
- package/dist/chunk-KNR2P2UY.js.map +0 -1
- package/dist/chunk-LRQNPYCK.js.map +0 -1
- package/dist/chunk-LW75SKZR.js.map +0 -1
- package/dist/chunk-SFMIMLXR.js +0 -63
- package/dist/chunk-SFMIMLXR.js.map +0 -1
- package/dist/chunk-XSNW6Y3X.js.map +0 -1
- package/dist/chunk-YJPCS2GP.js.map +0 -1
- package/dist/chunk-YNSTIMCF.js.map +0 -1
- package/dist/config-GEZFLXK6.js +0 -12
- package/dist/main-SFC5LV4E.js.map +0 -1
- package/dist/summaries-32PHOB6L.js +0 -20
- /package/dist/{config-GEZFLXK6.js.map → config-S2BDMT4B.js.map} +0 -0
- /package/dist/{install-EU35ERK5.js.map → file-vectors-VREL75MA.js.map} +0 -0
- /package/dist/{lean-43ADYTBW.js.map → index-facts-MBJKBYA3.js.map} +0 -0
- /package/dist/{operations-6Z6ZDVBP.js.map → index-pass-RP23G2CR.js.map} +0 -0
- /package/dist/{runtime-W5AW32SD.js.map → install-MCJIJMFF.js.map} +0 -0
- /package/dist/{summaries-32PHOB6L.js.map → lean-CBHVW6JM.js.map} +0 -0
- /package/dist/{zvec-24HLAFGN.js.map → local-TH5XCLVJ.js.map} +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readConfigFile,
|
|
3
|
+
setConfigBlock
|
|
4
|
+
} from "./chunk-Y4QQML24.js";
|
|
5
|
+
import {
|
|
6
|
+
LLAMA_RUNTIME,
|
|
7
|
+
LOCAL_MODELS,
|
|
8
|
+
checkLocalModelResources,
|
|
9
|
+
downloadLocalModel,
|
|
10
|
+
formatBytes,
|
|
11
|
+
freeMemory,
|
|
12
|
+
installedLocalModel,
|
|
13
|
+
localModelPath,
|
|
14
|
+
localRuntimeAvailable,
|
|
15
|
+
removeLocalModels
|
|
16
|
+
} from "./chunk-NNJSAYHY.js";
|
|
17
|
+
|
|
18
|
+
// src/cli/commands/local-llm.ts
|
|
19
|
+
import { spawn } from "child_process";
|
|
20
|
+
import { confirm, isCancel, spinner } from "@clack/prompts";
|
|
21
|
+
import pc from "picocolors";
|
|
22
|
+
var ok = (text) => `${pc.green("\u2713")} ${text}`;
|
|
23
|
+
var warn = (text) => `${pc.yellow("!")} ${text}`;
|
|
24
|
+
var row = (label, value) => ` ${label.padEnd(10)} ${value}`;
|
|
25
|
+
var LOCAL_LLM_SIZES = Object.keys(LOCAL_MODELS);
|
|
26
|
+
async function addLocalLlm(options = {}) {
|
|
27
|
+
const spec = LOCAL_MODELS[options.size ?? "default"];
|
|
28
|
+
const interactive = options.interactive ?? process.stdin.isTTY === true;
|
|
29
|
+
console.log(
|
|
30
|
+
`${pc.bold("Local LLM")} ${spec.id} ${pc.dim(`(${formatBytes(spec.bytes)} download, ~${formatBytes(spec.ramBytes)} RAM while carding)`)}`
|
|
31
|
+
);
|
|
32
|
+
const resources = await checkLocalModelResources(spec);
|
|
33
|
+
if (!resources.ok) {
|
|
34
|
+
console.log(warn(resources.reason));
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (freeMemory() < spec.ramBytes) {
|
|
38
|
+
console.log(warn(`only ${formatBytes(freeMemory())} RAM free right now; carding may be slow`));
|
|
39
|
+
}
|
|
40
|
+
if (!await localRuntimeAvailable()) {
|
|
41
|
+
const install = options.yes ? true : interactive ? await confirm({
|
|
42
|
+
message: `Install the local runtime (${LLAMA_RUNTIME}, ~20\u201335 MB prebuilt) with npm -g?`,
|
|
43
|
+
initialValue: true
|
|
44
|
+
}) : false;
|
|
45
|
+
if (isCancel(install) || !install) {
|
|
46
|
+
console.log(warn(`Runtime missing. Install it with: npm install -g ${LLAMA_RUNTIME}`));
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const code = await new Promise((resolve) => {
|
|
50
|
+
const child = spawn("npm", ["install", "-g", LLAMA_RUNTIME], { stdio: "inherit" });
|
|
51
|
+
child.on("error", () => resolve(1));
|
|
52
|
+
child.on("close", (exit) => resolve(exit ?? 1));
|
|
53
|
+
});
|
|
54
|
+
if (code !== 0 || !await localRuntimeAvailable()) {
|
|
55
|
+
console.log(warn(`Runtime install failed. Run: npm install -g ${LLAMA_RUNTIME}`));
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if ((await installedLocalModel(spec.size))?.id !== spec.id) {
|
|
60
|
+
const progress = spinner();
|
|
61
|
+
progress.start(`Downloading ${spec.file}`);
|
|
62
|
+
let shown = 0;
|
|
63
|
+
try {
|
|
64
|
+
await downloadLocalModel(spec, (received, total) => {
|
|
65
|
+
const percent = Math.floor(received / total * 100);
|
|
66
|
+
if (percent !== shown) {
|
|
67
|
+
shown = percent;
|
|
68
|
+
progress.message(`Downloading ${spec.file} ${percent}%`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
progress.stop(ok(`${spec.id} verified ${pc.dim(localModelPath(spec))}`));
|
|
72
|
+
} catch (error) {
|
|
73
|
+
progress.stop(warn(error.message));
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
console.log(ok(`${spec.id} already installed`));
|
|
78
|
+
}
|
|
79
|
+
const llm = (await readConfigFile()).llm;
|
|
80
|
+
if (llm === void 0 || llm.provider === "local" || llm.enabled === false) {
|
|
81
|
+
await setConfigBlock("llm", { ...llm, enabled: true, provider: "local", size: spec.size });
|
|
82
|
+
} else {
|
|
83
|
+
console.log(
|
|
84
|
+
pc.dim(
|
|
85
|
+
` .codebuddy/config.json uses ${llm.provider}; set llm.provider to "local" to use it.`
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
console.log(
|
|
90
|
+
pc.dim(
|
|
91
|
+
" Cards are written in the background by `codebuddy serve` or `codebuddy index --cards`."
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
async function removeLocalLlm() {
|
|
97
|
+
const removed = await removeLocalModels();
|
|
98
|
+
const llm = (await readConfigFile()).llm;
|
|
99
|
+
if (llm?.provider === "local") await setConfigBlock("llm", { ...llm, enabled: false });
|
|
100
|
+
console.log(
|
|
101
|
+
removed.length > 0 ? ok(`removed ${removed.join(", ")}; CodeBuddy falls back to 2.14 behaviour`) : pc.dim("no local LLM installed")
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
async function printLocalLlmStatus(cards) {
|
|
105
|
+
const llm = (await readConfigFile()).llm;
|
|
106
|
+
const installed = await installedLocalModel(llm?.size);
|
|
107
|
+
console.log(pc.bold("Local LLM"));
|
|
108
|
+
if (!installed) {
|
|
109
|
+
console.log(row("model", pc.dim("not installed (optional: codebuddy models add local-llm)")));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const runtime = await localRuntimeAvailable();
|
|
113
|
+
console.log(row("model", ok(`${installed.id} ${pc.dim(formatBytes(installed.bytes))}`)));
|
|
114
|
+
console.log(
|
|
115
|
+
row("runtime", runtime ? ok(LLAMA_RUNTIME) : warn(`missing: npm install -g ${LLAMA_RUNTIME}`))
|
|
116
|
+
);
|
|
117
|
+
if (llm?.enabled === false)
|
|
118
|
+
console.log(row("status", warn("disabled in .codebuddy/config.json")));
|
|
119
|
+
if (!cards) return;
|
|
120
|
+
const pass = cards.lastPass;
|
|
121
|
+
const detail = cards.writer ? `being written by pid ${cards.writer}` : pass && `last pass: ${pass.summarized} written${pass.timedOut ? `, ${pass.timedOut} timed out` : ""}, ${pass.remaining} left (${pass.stoppedBy})`;
|
|
122
|
+
console.log(
|
|
123
|
+
row(
|
|
124
|
+
"cards",
|
|
125
|
+
`${cards.carded} / ${cards.eligible} source files${detail ? pc.dim(` (${detail})`) : ""}`
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
LOCAL_LLM_SIZES,
|
|
132
|
+
addLocalLlm,
|
|
133
|
+
removeLocalLlm,
|
|
134
|
+
printLocalLlmStatus
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=chunk-K4XNTYV7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/commands/local-llm.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { confirm, isCancel, spinner } from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport { readConfigFile, setConfigBlock } from \"../../core/config-file.js\";\nimport {\n checkLocalModelResources,\n downloadLocalModel,\n formatBytes,\n freeMemory,\n installedLocalModel,\n LLAMA_RUNTIME,\n LOCAL_MODELS,\n type LocalModelSize,\n localModelPath,\n localRuntimeAvailable,\n removeLocalModels,\n} from \"../../llm/local.js\";\nimport type { cardProgress } from \"../../llm/summaries.js\";\n\nconst ok = (text: string) => `${pc.green(\"✓\")} ${text}`;\nconst warn = (text: string) => `${pc.yellow(\"!\")} ${text}`;\nconst row = (label: string, value: string) => ` ${label.padEnd(10)} ${value}`;\n\nexport const LOCAL_LLM_SIZES = Object.keys(LOCAL_MODELS) as LocalModelSize[];\n\n/**\n * `codebuddy models add local-llm`: the explicit opt-in for the local small\n * model. Shows the size, checks disk and RAM, installs the node-llama-cpp\n * runtime when missing (after confirmation), downloads the pinned GGUF with a\n * checksum check, and records the size in `.codebuddy/config.json`.\n */\nexport async function addLocalLlm(\n options: { size?: LocalModelSize; yes?: boolean; interactive?: boolean } = {},\n): Promise<boolean> {\n const spec = LOCAL_MODELS[options.size ?? \"default\"];\n const interactive = options.interactive ?? process.stdin.isTTY === true;\n console.log(\n `${pc.bold(\"Local LLM\")} ${spec.id} ${pc.dim(`(${formatBytes(spec.bytes)} download, ~${formatBytes(spec.ramBytes)} RAM while carding)`)}`,\n );\n const resources = await checkLocalModelResources(spec);\n if (!resources.ok) {\n console.log(warn(resources.reason));\n return false;\n }\n if (freeMemory() < spec.ramBytes) {\n console.log(warn(`only ${formatBytes(freeMemory())} RAM free right now; carding may be slow`));\n }\n if (!(await localRuntimeAvailable())) {\n const install = options.yes\n ? true\n : interactive\n ? await confirm({\n message: `Install the local runtime (${LLAMA_RUNTIME}, ~20–35 MB prebuilt) with npm -g?`,\n initialValue: true,\n })\n : false;\n if (isCancel(install) || !install) {\n console.log(warn(`Runtime missing. Install it with: npm install -g ${LLAMA_RUNTIME}`));\n return false;\n }\n const code = await new Promise<number>((resolve) => {\n const child = spawn(\"npm\", [\"install\", \"-g\", LLAMA_RUNTIME], { stdio: \"inherit\" });\n child.on(\"error\", () => resolve(1));\n child.on(\"close\", (exit) => resolve(exit ?? 1));\n });\n if (code !== 0 || !(await localRuntimeAvailable())) {\n console.log(warn(`Runtime install failed. Run: npm install -g ${LLAMA_RUNTIME}`));\n return false;\n }\n }\n if ((await installedLocalModel(spec.size))?.id !== spec.id) {\n const progress = spinner();\n progress.start(`Downloading ${spec.file}`);\n let shown = 0;\n try {\n await downloadLocalModel(spec, (received, total) => {\n const percent = Math.floor((received / total) * 100);\n if (percent !== shown) {\n shown = percent;\n progress.message(`Downloading ${spec.file} ${percent}%`);\n }\n });\n progress.stop(ok(`${spec.id} verified ${pc.dim(localModelPath(spec))}`));\n } catch (error) {\n progress.stop(warn((error as Error).message));\n return false;\n }\n } else {\n console.log(ok(`${spec.id} already installed`));\n }\n const llm = (await readConfigFile()).llm;\n // A BYOK block stays as it is (the local model is then unused until it is switched).\n if (llm === undefined || llm.provider === \"local\" || llm.enabled === false) {\n await setConfigBlock(\"llm\", { ...llm, enabled: true, provider: \"local\", size: spec.size });\n } else {\n console.log(\n pc.dim(\n ` .codebuddy/config.json uses ${llm.provider}; set llm.provider to \"local\" to use it.`,\n ),\n );\n }\n console.log(\n pc.dim(\n \" Cards are written in the background by `codebuddy serve` or `codebuddy index --cards`.\",\n ),\n );\n return true;\n}\n\nexport async function removeLocalLlm(): Promise<void> {\n const removed = await removeLocalModels();\n const llm = (await readConfigFile()).llm;\n if (llm?.provider === \"local\") await setConfigBlock(\"llm\", { ...llm, enabled: false });\n console.log(\n removed.length > 0\n ? ok(`removed ${removed.join(\", \")}; CodeBuddy falls back to 2.14 behaviour`)\n : pc.dim(\"no local LLM installed\"),\n );\n}\n\nexport async function printLocalLlmStatus(\n cards: Awaited<ReturnType<typeof cardProgress>> | null,\n): Promise<void> {\n const llm = (await readConfigFile()).llm;\n const installed = await installedLocalModel(llm?.size);\n console.log(pc.bold(\"Local LLM\"));\n if (!installed) {\n console.log(row(\"model\", pc.dim(\"not installed (optional: codebuddy models add local-llm)\")));\n return;\n }\n const runtime = await localRuntimeAvailable();\n console.log(row(\"model\", ok(`${installed.id} ${pc.dim(formatBytes(installed.bytes))}`)));\n console.log(\n row(\"runtime\", runtime ? ok(LLAMA_RUNTIME) : warn(`missing: npm install -g ${LLAMA_RUNTIME}`)),\n );\n if (llm?.enabled === false)\n console.log(row(\"status\", warn(\"disabled in .codebuddy/config.json\")));\n if (!cards) return;\n const pass = cards.lastPass;\n const detail = cards.writer\n ? `being written by pid ${cards.writer}`\n : pass &&\n `last pass: ${pass.summarized} written${pass.timedOut ? `, ${pass.timedOut} timed out` : \"\"}, ${pass.remaining} left (${pass.stoppedBy})`;\n console.log(\n row(\n \"cards\",\n `${cards.carded} / ${cards.eligible} source files${detail ? pc.dim(` (${detail})`) : \"\"}`,\n ),\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
redactLlmKeys
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-RGIVHKBH.js";
|
|
4
4
|
import {
|
|
5
5
|
writeAtomic
|
|
6
6
|
} from "./chunk-CG5B3LYG.js";
|
|
7
|
+
import {
|
|
8
|
+
localChatJson
|
|
9
|
+
} from "./chunk-NNJSAYHY.js";
|
|
7
10
|
|
|
8
11
|
// src/llm/budget.ts
|
|
9
12
|
import { mkdir, readFile } from "fs/promises";
|
|
@@ -64,6 +67,7 @@ async function storeResponse(root, key, value) {
|
|
|
64
67
|
var DEFAULT_COOLDOWN_SECONDS = 60;
|
|
65
68
|
async function chatJson(config, request) {
|
|
66
69
|
const started = Date.now();
|
|
70
|
+
if (config.local) return localCall(config.local, request);
|
|
67
71
|
try {
|
|
68
72
|
return await call(config, request, started);
|
|
69
73
|
} catch (error) {
|
|
@@ -121,7 +125,9 @@ async function call(config, request, started) {
|
|
|
121
125
|
],
|
|
122
126
|
temperature: 0,
|
|
123
127
|
max_tokens: request.maxTokens,
|
|
124
|
-
response_format: { type: "json_object" }
|
|
128
|
+
response_format: { type: "json_object" },
|
|
129
|
+
// Qwen3 reasons by default on some hosts; cards and reranks need none.
|
|
130
|
+
...config.provider === "groq" && /^qwen\//.test(config.model) ? { reasoning_effort: "none" } : {}
|
|
125
131
|
}),
|
|
126
132
|
signal: AbortSignal.timeout(request.deadlineMs)
|
|
127
133
|
});
|
|
@@ -176,7 +182,28 @@ async function call(config, request, started) {
|
|
|
176
182
|
cached: false
|
|
177
183
|
};
|
|
178
184
|
}
|
|
179
|
-
function
|
|
185
|
+
async function localCall(spec, request) {
|
|
186
|
+
const started = Date.now();
|
|
187
|
+
const cacheKey = request.cacheKey && `local:${spec.id}:${request.cacheKey}`;
|
|
188
|
+
if (cacheKey) {
|
|
189
|
+
const parsed = request.schema.safeParse(await cachedResponse(request.repositoryRoot, cacheKey));
|
|
190
|
+
if (parsed.success) {
|
|
191
|
+
return {
|
|
192
|
+
ok: true,
|
|
193
|
+
value: parsed.data,
|
|
194
|
+
model: spec.id,
|
|
195
|
+
latencyMs: Date.now() - started,
|
|
196
|
+
tokens: 0,
|
|
197
|
+
cached: true
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const result = await localChatJson(spec, request);
|
|
202
|
+
if (result.ok && cacheKey) await storeResponse(request.repositoryRoot, cacheKey, result.value);
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
function parseJsonContent(raw) {
|
|
206
|
+
const content = raw.replace(/<think>[\s\S]*?<\/think>/g, "");
|
|
180
207
|
const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
|
|
181
208
|
try {
|
|
182
209
|
return JSON.parse((fenced ?? content).trim());
|
|
@@ -188,7 +215,8 @@ function estimateTokens(request, content) {
|
|
|
188
215
|
return Math.ceil((request.system.length + request.user.length + content.length) / 4);
|
|
189
216
|
}
|
|
190
217
|
function redact(config, text) {
|
|
191
|
-
|
|
218
|
+
const redacted = redactLlmKeys(text);
|
|
219
|
+
return config.apiKey ? redacted.split(config.apiKey).join("[REDACTED]") : redacted;
|
|
192
220
|
}
|
|
193
221
|
|
|
194
222
|
export {
|
|
@@ -196,4 +224,4 @@ export {
|
|
|
196
224
|
coolingDown,
|
|
197
225
|
chatJson
|
|
198
226
|
};
|
|
199
|
-
//# sourceMappingURL=chunk-
|
|
227
|
+
//# sourceMappingURL=chunk-MNSMW4MZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/llm/budget.ts","../src/llm/client.ts"],"sourcesContent":["import { mkdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { writeAtomic } from \"../core/markdown-store-fs.js\";\n\n/**\n * Free-tier hygiene for the LLM layer, all in `.codebuddy/cache/` (gitignored,\n * rebuildable): a per-day token ledger per provider:model, a cooldown after\n * 429s, and a response cache. Background work stops at 70% of the daily\n * budget so prompt-time calls keep a reserved 30%.\n *\n * ponytail: read-modify-write JSON without a lock; two hook processes can lose\n * one update. Fine for a soft budget; add the staging lock if it ever matters.\n */\nconst BACKGROUND_SHARE = 0.7;\nconst CACHE_LIMIT = 2_000;\n\nconst file = (root: string, name: string) => join(root, \".codebuddy\", \"cache\", name);\n\nasync function readJson<T>(path: string, fallback: T): Promise<T> {\n try {\n return JSON.parse(await readFile(path, \"utf8\")) as T;\n } catch {\n return fallback;\n }\n}\n\nasync function writeJson(path: string, value: unknown): Promise<void> {\n await mkdir(join(path, \"..\"), { recursive: true });\n await writeAtomic(path, `${JSON.stringify(value)}\\n`);\n}\n\ntype Ledger = { day: string; used: Record<string, number> };\nconst utcDay = (now: Date) => now.toISOString().slice(0, 10);\n\nexport async function usageToday(root: string, now = new Date()): Promise<Record<string, number>> {\n const ledger = await readJson<Ledger>(file(root, \"llm-usage.json\"), { day: \"\", used: {} });\n return ledger.day === utcDay(now) ? ledger.used : {};\n}\n\nexport async function recordUsage(\n root: string,\n key: string,\n tokens: number,\n now = new Date(),\n): Promise<void> {\n const used = await usageToday(root, now);\n used[key] = (used[key] ?? 0) + tokens;\n await writeJson(file(root, \"llm-usage.json\"), { day: utcDay(now), used });\n}\n\n/** Whether `estimate` more tokens fit today's share for this purpose. */\nexport async function reserveBudget(\n root: string,\n key: string,\n purpose: \"prompt\" | \"background\",\n estimate: number,\n dailyBudget: number,\n now = new Date(),\n): Promise<boolean> {\n const limit = purpose === \"background\" ? dailyBudget * BACKGROUND_SHARE : dailyBudget;\n return ((await usageToday(root, now))[key] ?? 0) + estimate <= limit;\n}\n\n/** Milliseconds of cooldown left for this provider:model (0 = none). */\nexport async function coolingDown(root: string, key: string, now = new Date()): Promise<number> {\n const until = (await readJson<Record<string, number>>(file(root, \"llm-cooldown.json\"), {}))[key];\n return until ? Math.max(0, until - now.getTime()) : 0;\n}\n\nexport async function startCooldown(\n root: string,\n key: string,\n seconds: number,\n now = new Date(),\n): Promise<void> {\n const path = file(root, \"llm-cooldown.json\");\n const cooldowns = await readJson<Record<string, number>>(path, {});\n cooldowns[key] = now.getTime() + seconds * 1_000;\n await writeJson(path, cooldowns);\n}\n\nexport async function cachedResponse(root: string, key: string): Promise<unknown | undefined> {\n return (await readJson<Record<string, unknown>>(file(root, \"llm-responses.json\"), {}))[key];\n}\n\n/** Insertion-ordered; the oldest entries go once the cache holds CACHE_LIMIT. */\nexport async function storeResponse(root: string, key: string, value: unknown): Promise<void> {\n const path = file(root, \"llm-responses.json\");\n const cache = await readJson<Record<string, unknown>>(path, {});\n delete cache[key];\n cache[key] = value;\n const keys = Object.keys(cache);\n for (const old of keys.slice(0, Math.max(0, keys.length - CACHE_LIMIT))) delete cache[old];\n await writeJson(path, cache);\n}\n","import type { z } from \"zod\";\nimport {\n cachedResponse,\n coolingDown,\n recordUsage,\n reserveBudget,\n startCooldown,\n storeResponse,\n} from \"./budget.js\";\nimport { type EnabledLlmConfig, redactLlmKeys } from \"./config.js\";\nimport { type LocalModelSpec, localChatJson } from \"./local.js\";\n\n/** Seconds to back off after a 429 without a usable retry-after header. */\nconst DEFAULT_COOLDOWN_SECONDS = 60;\n\n/**\n * One OpenAI-compatible chat call that must answer with JSON (Groq and\n * OpenRouter share the API). It never throws: every failure is a reason the\n * caller turns into today's deterministic behaviour plus an explain line.\n */\nexport type LlmFailure =\n | \"disabled\"\n | \"timeout\"\n | \"rate-limited\"\n | \"cooldown\"\n | \"budget\"\n | \"http\"\n | \"invalid\";\n\nexport type LlmResult<T> =\n | { ok: true; value: T; model: string; latencyMs: number; tokens: number; cached: boolean }\n | { ok: false; reason: LlmFailure; detail: string; latencyMs: number };\n\nexport type ChatJsonRequest<T> = {\n system: string;\n user: string;\n schema: z.ZodType<T>;\n maxTokens: number;\n deadlineMs: number;\n /** Prompt-path calls get the reserved budget share; background work stops earlier. */\n purpose: \"prompt\" | \"background\";\n /**\n * Local backend only: a JSON schema for the generation grammar (with\n * `maxItems`/`maxLength` bounds) instead of one derived from `schema`.\n */\n jsonSchema?: object;\n /** Cache identity (inputs + prompt version); omitted = never cached. */\n cacheKey?: string;\n repositoryRoot: string;\n};\n\nexport async function chatJson<T>(\n config: EnabledLlmConfig,\n request: ChatJsonRequest<T>,\n): Promise<LlmResult<T>> {\n const started = Date.now();\n if (config.local) return localCall(config.local, request);\n try {\n return await call(config, request, started);\n } catch (error) {\n // Structural guarantee: an unexpected body shape is an invalid answer, not a crash.\n return {\n ok: false,\n reason: \"invalid\",\n detail: redact(config, String((error as Error)?.message ?? error)).slice(0, 300),\n latencyMs: Date.now() - started,\n };\n }\n}\n\nasync function call<T>(\n config: EnabledLlmConfig,\n request: ChatJsonRequest<T>,\n started: number,\n): Promise<LlmResult<T>> {\n const fail = (reason: LlmFailure, detail: string): LlmResult<T> => ({\n ok: false,\n reason,\n detail: redact(config, detail).slice(0, 300),\n latencyMs: Date.now() - started,\n });\n const root = request.repositoryRoot;\n const ledgerKey = `${config.provider}:${config.model}`;\n const cooling = await coolingDown(root, ledgerKey);\n if (cooling > 0) return fail(\"cooldown\", `rate limited; retry in ${Math.ceil(cooling / 1000)} s`);\n const cacheKey = request.cacheKey && `${ledgerKey}:${request.cacheKey}`;\n if (cacheKey) {\n const parsed = request.schema.safeParse(await cachedResponse(root, cacheKey));\n if (parsed.success) {\n return {\n ok: true,\n value: parsed.data,\n model: config.model,\n latencyMs: Date.now() - started,\n tokens: 0,\n cached: true,\n };\n }\n }\n const estimate = estimateTokens(request, \"\") + request.maxTokens;\n if (!(await reserveBudget(root, ledgerKey, request.purpose, estimate, config.dailyTokenBudget))) {\n return fail(\"budget\", `daily ${request.purpose} budget for ${ledgerKey} is spent`);\n }\n let response: Response;\n try {\n response = await fetch(`${config.baseUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n authorization: `Bearer ${config.apiKey}`,\n \"content-type\": \"application/json\",\n ...(config.provider === \"openrouter\" ? { \"x-title\": \"CodeBuddy\" } : {}),\n },\n body: JSON.stringify({\n model: config.model,\n messages: [\n { role: \"system\", content: request.system },\n { role: \"user\", content: request.user },\n ],\n temperature: 0,\n max_tokens: request.maxTokens,\n response_format: { type: \"json_object\" },\n // Qwen3 reasons by default on some hosts; cards and reranks need none.\n ...(config.provider === \"groq\" && /^qwen\\//.test(config.model)\n ? { reasoning_effort: \"none\" }\n : {}),\n }),\n signal: AbortSignal.timeout(request.deadlineMs),\n });\n } catch (error) {\n const name = (error as Error)?.name;\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n return fail(\"timeout\", `no answer within ${request.deadlineMs} ms`);\n }\n return fail(\"http\", (error as Error)?.message ?? String(error));\n }\n let body: string;\n try {\n body = await response.text();\n } catch (error) {\n const name = (error as Error)?.name;\n return fail(\n name === \"TimeoutError\" || name === \"AbortError\" ? \"timeout\" : \"http\",\n String(error),\n );\n }\n if (response.status === 429) {\n const retryAfter = Number(response.headers.get(\"retry-after\"));\n await startCooldown(\n root,\n ledgerKey,\n Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : DEFAULT_COOLDOWN_SECONDS,\n );\n return fail(\"rate-limited\", body);\n }\n if (!response.ok) return fail(\"http\", `HTTP ${response.status}: ${body}`);\n\n let completion: {\n model?: unknown;\n choices?: Array<{ message?: { content?: unknown } }>;\n usage?: { total_tokens?: unknown };\n } | null;\n try {\n completion = JSON.parse(body);\n } catch {\n return fail(\"invalid\", \"response is not JSON\");\n }\n const raw = completion?.choices?.[0]?.message?.content;\n if (typeof raw !== \"string\") return fail(\"invalid\", \"answer has no text content\");\n const content = raw;\n const reported = completion?.usage?.total_tokens;\n const tokens =\n typeof reported === \"number\" && Number.isFinite(reported)\n ? reported\n : estimateTokens(request, content);\n await recordUsage(root, ledgerKey, tokens);\n const parsed = request.schema.safeParse(parseJsonContent(content));\n if (!parsed.success) return fail(\"invalid\", `output failed validation: ${content.slice(0, 120)}`);\n if (cacheKey) await storeResponse(root, cacheKey, parsed.data);\n return {\n ok: true,\n value: parsed.data,\n model: typeof completion?.model === \"string\" ? completion.model : config.model,\n latencyMs: Date.now() - started,\n tokens,\n cached: false,\n };\n}\n\n/** Local generation: same response cache, no budget ledger or cooldown (no cost, no quota). */\nasync function localCall<T>(\n spec: LocalModelSpec,\n request: ChatJsonRequest<T>,\n): Promise<LlmResult<T>> {\n const started = Date.now();\n const cacheKey = request.cacheKey && `local:${spec.id}:${request.cacheKey}`;\n if (cacheKey) {\n const parsed = request.schema.safeParse(await cachedResponse(request.repositoryRoot, cacheKey));\n if (parsed.success) {\n return {\n ok: true,\n value: parsed.data,\n model: spec.id,\n latencyMs: Date.now() - started,\n tokens: 0,\n cached: true,\n };\n }\n }\n const result = await localChatJson(spec, request);\n if (result.ok && cacheKey) await storeResponse(request.repositoryRoot, cacheKey, result.value);\n return result;\n}\n\n/** Free models often wrap JSON in a ```json fence despite json_object mode. */\nfunction parseJsonContent(raw: string): unknown {\n // A Qwen3 host may still prepend an (empty) reasoning block.\n const content = raw.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\");\n const fenced = content.match(/```(?:json)?\\s*([\\s\\S]*?)```/i)?.[1];\n try {\n return JSON.parse((fenced ?? content).trim());\n } catch {\n return undefined;\n }\n}\n\nfunction estimateTokens(request: { system: string; user: string }, content: string): number {\n return Math.ceil((request.system.length + request.user.length + content.length) / 4);\n}\n\nfunction redact(config: EnabledLlmConfig, text: string): string {\n const redacted = redactLlmKeys(text);\n return config.apiKey ? redacted.split(config.apiKey).join(\"[REDACTED]\") : redacted;\n}\n"],"mappings":"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","names":["parsed"]}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import {
|
|
2
|
+
modelCacheDir
|
|
3
|
+
} from "./chunk-3QGW53GD.js";
|
|
4
|
+
|
|
5
|
+
// src/llm/local.ts
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
import { createWriteStream } from "fs";
|
|
8
|
+
import { mkdir, rename, rm, stat, statfs, utimes, writeFile } from "fs/promises";
|
|
9
|
+
import { availableParallelism, freemem, totalmem } from "os";
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
import { Readable } from "stream";
|
|
12
|
+
import { pipeline } from "stream/promises";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
var GB = 1024 ** 3;
|
|
15
|
+
var LOCAL_MODELS = {
|
|
16
|
+
default: {
|
|
17
|
+
size: "default",
|
|
18
|
+
id: "qwen3-0.6b-q4_k_m",
|
|
19
|
+
repo: "unsloth/Qwen3-0.6B-GGUF",
|
|
20
|
+
revision: "50968a4468ef4233ed78cd7c3de230dd1d61a56b",
|
|
21
|
+
file: "Qwen3-0.6B-Q4_K_M.gguf",
|
|
22
|
+
bytes: 396705472,
|
|
23
|
+
sha256: "ac2d97712095a558e31573f62f466a3f9d93990898b0ec79d7c974c1780d524a",
|
|
24
|
+
ramBytes: 1 * GB,
|
|
25
|
+
noThink: true
|
|
26
|
+
},
|
|
27
|
+
better: {
|
|
28
|
+
size: "better",
|
|
29
|
+
id: "qwen3-1.7b-q4_k_m",
|
|
30
|
+
repo: "unsloth/Qwen3-1.7B-GGUF",
|
|
31
|
+
revision: "d7f544eead698dbd1f15126ef60b45a1e1933222",
|
|
32
|
+
file: "Qwen3-1.7B-Q4_K_M.gguf",
|
|
33
|
+
bytes: 1107409472,
|
|
34
|
+
sha256: "b139949c5bd74937ad8ed8c8cf3d9ffb1e99c866c823204dc42c0d91fa181897",
|
|
35
|
+
ramBytes: 2.2 * GB,
|
|
36
|
+
noThink: true
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var LLAMA_RUNTIME = "node-llama-cpp@3.22.3";
|
|
40
|
+
function localModelPath(spec) {
|
|
41
|
+
return join(modelCacheDir(), "gguf", spec.file);
|
|
42
|
+
}
|
|
43
|
+
async function installedLocalModel(preferred) {
|
|
44
|
+
if (process.env.CODEBUDDY_LOCAL_LLM === "0") return null;
|
|
45
|
+
const order = [...preferred ? [preferred] : [], "default", "better"];
|
|
46
|
+
for (const size of new Set(order)) {
|
|
47
|
+
const spec = LOCAL_MODELS[size];
|
|
48
|
+
const info = await stat(localModelPath(spec)).catch(() => null);
|
|
49
|
+
if (info?.isFile() && info.size === spec.bytes) return spec;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
async function localRuntimeAvailable() {
|
|
54
|
+
try {
|
|
55
|
+
await importRuntime();
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function machineCanCard() {
|
|
62
|
+
if (totalmem() < 4 * GB) {
|
|
63
|
+
return { ok: false, reason: `${formatBytes(totalmem())} RAM (setup installs it from 4 GB)` };
|
|
64
|
+
}
|
|
65
|
+
if (availableParallelism() < 2) return { ok: false, reason: "a single CPU core" };
|
|
66
|
+
return { ok: true };
|
|
67
|
+
}
|
|
68
|
+
async function checkLocalModelResources(spec) {
|
|
69
|
+
const directory = modelCacheDir();
|
|
70
|
+
await mkdir(directory, { recursive: true });
|
|
71
|
+
const fs = await statfs(directory).catch(() => null);
|
|
72
|
+
if (fs && fs.bavail * fs.bsize < spec.bytes * 1.1) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
reason: `needs ${formatBytes(spec.bytes * 1.1)} free in ${directory}, ${formatBytes(fs.bavail * fs.bsize)} available`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (totalmem() < spec.ramBytes * 2) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
reason: `needs about ${formatBytes(spec.ramBytes)} of RAM while loaded; this machine has ${formatBytes(totalmem())} in total`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return { ok: true };
|
|
85
|
+
}
|
|
86
|
+
async function downloadLocalModel(spec, onProgress, fetchImpl = fetch) {
|
|
87
|
+
const target = localModelPath(spec);
|
|
88
|
+
await mkdir(dirname(target), { recursive: true });
|
|
89
|
+
const partial = `${target}.partial`;
|
|
90
|
+
const url = `https://huggingface.co/${spec.repo}/resolve/${spec.revision}/${spec.file}`;
|
|
91
|
+
const response = await fetchImpl(url);
|
|
92
|
+
if (!response.ok || !response.body) {
|
|
93
|
+
throw new Error(`download failed: HTTP ${response.status} for ${url}`);
|
|
94
|
+
}
|
|
95
|
+
const hash = createHash("sha256");
|
|
96
|
+
let received = 0;
|
|
97
|
+
const source = Readable.fromWeb(response.body);
|
|
98
|
+
source.on("data", (chunk) => {
|
|
99
|
+
hash.update(chunk);
|
|
100
|
+
received += chunk.length;
|
|
101
|
+
onProgress?.(received, spec.bytes);
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
await pipeline(source, createWriteStream(partial));
|
|
105
|
+
const digest = hash.digest("hex");
|
|
106
|
+
if (received !== spec.bytes || digest !== spec.sha256) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`checksum mismatch for ${spec.file} (got ${received} bytes, sha256 ${digest.slice(0, 12)}\u2026); not installed`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
await rename(partial, target);
|
|
112
|
+
} finally {
|
|
113
|
+
await rm(partial, { force: true });
|
|
114
|
+
}
|
|
115
|
+
return target;
|
|
116
|
+
}
|
|
117
|
+
async function removeLocalModels() {
|
|
118
|
+
const removed = [];
|
|
119
|
+
for (const spec of Object.values(LOCAL_MODELS)) {
|
|
120
|
+
const path = localModelPath(spec);
|
|
121
|
+
if (await stat(path).catch(() => null)) {
|
|
122
|
+
await rm(path, { force: true });
|
|
123
|
+
removed.push(spec.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return removed;
|
|
127
|
+
}
|
|
128
|
+
var loading = null;
|
|
129
|
+
var loaded = null;
|
|
130
|
+
var queue = Promise.resolve();
|
|
131
|
+
var grammars = /* @__PURE__ */ new Map();
|
|
132
|
+
async function importRuntime() {
|
|
133
|
+
const specifier = process.env.CODEBUDDY_LLAMA_RUNTIME ?? "node-llama-cpp";
|
|
134
|
+
return await import(specifier);
|
|
135
|
+
}
|
|
136
|
+
var CONTEXT_TOKENS = 4096;
|
|
137
|
+
function generationThreads() {
|
|
138
|
+
const configured = Number(process.env.CODEBUDDY_LLM_THREADS);
|
|
139
|
+
if (Number.isInteger(configured) && configured > 0) return configured;
|
|
140
|
+
return Math.max(1, availableParallelism() - 1);
|
|
141
|
+
}
|
|
142
|
+
async function load(spec) {
|
|
143
|
+
if (loaded?.spec.id === spec.id) return loaded;
|
|
144
|
+
if (!loading) {
|
|
145
|
+
loading = (async () => {
|
|
146
|
+
const runtime = await importRuntime();
|
|
147
|
+
const llama = await runtime.getLlama({ gpu: "auto" });
|
|
148
|
+
const model = await llama.loadModel({ modelPath: localModelPath(spec) });
|
|
149
|
+
const context = await model.createContext({
|
|
150
|
+
contextSize: CONTEXT_TOKENS,
|
|
151
|
+
threads: generationThreads()
|
|
152
|
+
});
|
|
153
|
+
loaded = { spec, llama, model, context, sequence: context.getSequence(), runtime };
|
|
154
|
+
return loaded;
|
|
155
|
+
})().finally(() => {
|
|
156
|
+
loading = null;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return loading;
|
|
160
|
+
}
|
|
161
|
+
var IDLE_UNLOAD_MS = 6e4;
|
|
162
|
+
var idleTimer = null;
|
|
163
|
+
var running = 0;
|
|
164
|
+
function scheduleUnload() {
|
|
165
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
166
|
+
idleTimer = setTimeout(() => void unloadLocalModel(), IDLE_UNLOAD_MS);
|
|
167
|
+
idleTimer.unref();
|
|
168
|
+
}
|
|
169
|
+
async function unloadLocalModel() {
|
|
170
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
171
|
+
idleTimer = null;
|
|
172
|
+
const current = loaded;
|
|
173
|
+
if (!current || running > 0) return;
|
|
174
|
+
loaded = null;
|
|
175
|
+
grammars.clear();
|
|
176
|
+
await current.context.dispose().catch(() => {
|
|
177
|
+
});
|
|
178
|
+
await current.model.dispose().catch(() => {
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function isLocalModelWarm() {
|
|
182
|
+
return loaded !== null;
|
|
183
|
+
}
|
|
184
|
+
async function warmLocalModel(spec) {
|
|
185
|
+
try {
|
|
186
|
+
await load(spec);
|
|
187
|
+
return true;
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function localChatJson(spec, request) {
|
|
193
|
+
const started = Date.now();
|
|
194
|
+
const fail = (reason, detail) => ({
|
|
195
|
+
ok: false,
|
|
196
|
+
reason,
|
|
197
|
+
detail: detail.slice(0, 300),
|
|
198
|
+
latencyMs: Date.now() - started
|
|
199
|
+
});
|
|
200
|
+
let model;
|
|
201
|
+
try {
|
|
202
|
+
model = await withDeadline(load(spec), request.deadlineMs);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return error.name === "TimeoutError" ? fail("timeout", "model still loading") : fail("disabled", `local model unavailable: ${error.message}`);
|
|
205
|
+
}
|
|
206
|
+
const run = async () => {
|
|
207
|
+
running++;
|
|
208
|
+
try {
|
|
209
|
+
return await generate();
|
|
210
|
+
} finally {
|
|
211
|
+
running--;
|
|
212
|
+
scheduleUnload();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const generate = async () => {
|
|
216
|
+
if (!loaded) model = await load(spec);
|
|
217
|
+
const remaining = request.deadlineMs - (Date.now() - started);
|
|
218
|
+
if (remaining <= 0) return fail("timeout", `no answer within ${request.deadlineMs} ms`);
|
|
219
|
+
const grammar = await grammarFor(model.llama, request.jsonSchema ?? request.schema);
|
|
220
|
+
const session = new model.runtime.LlamaChatSession({
|
|
221
|
+
contextSequence: model.sequence,
|
|
222
|
+
autoDisposeSequence: false,
|
|
223
|
+
systemPrompt: request.system
|
|
224
|
+
});
|
|
225
|
+
const controller = new AbortController();
|
|
226
|
+
const timer = setTimeout(() => controller.abort(), remaining);
|
|
227
|
+
try {
|
|
228
|
+
const text = await session.prompt(`${spec.noThink ? "/no_think\n" : ""}${request.user}`, {
|
|
229
|
+
grammar,
|
|
230
|
+
maxTokens: request.maxTokens,
|
|
231
|
+
temperature: 0,
|
|
232
|
+
...spec.noThink ? { budgets: { thoughtTokens: 0 } } : {},
|
|
233
|
+
signal: controller.signal,
|
|
234
|
+
stopOnAbortSignal: true
|
|
235
|
+
});
|
|
236
|
+
if (controller.signal.aborted) {
|
|
237
|
+
return fail("timeout", `no answer within ${request.deadlineMs} ms`);
|
|
238
|
+
}
|
|
239
|
+
let value;
|
|
240
|
+
try {
|
|
241
|
+
value = JSON.parse(text);
|
|
242
|
+
} catch {
|
|
243
|
+
return fail("invalid", `output is not JSON: ${text.slice(0, 120)}`);
|
|
244
|
+
}
|
|
245
|
+
const parsed = request.schema.safeParse(value);
|
|
246
|
+
if (!parsed.success)
|
|
247
|
+
return fail("invalid", `output failed validation: ${text.slice(0, 120)}`);
|
|
248
|
+
return {
|
|
249
|
+
ok: true,
|
|
250
|
+
value: parsed.data,
|
|
251
|
+
model: spec.id,
|
|
252
|
+
latencyMs: Date.now() - started,
|
|
253
|
+
tokens: Math.ceil((request.system.length + request.user.length + text.length) / 4),
|
|
254
|
+
cached: false
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
return controller.signal.aborted ? fail("timeout", `no answer within ${request.deadlineMs} ms`) : fail("invalid", String(error?.message ?? error));
|
|
258
|
+
} finally {
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
session.dispose({ disposeSequence: false });
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
const result = queue.then(run, run);
|
|
264
|
+
queue = result.catch(() => void 0);
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
async function grammarFor(llama, schema) {
|
|
268
|
+
let grammar = grammars.get(schema);
|
|
269
|
+
if (!grammar) {
|
|
270
|
+
grammar = (async () => {
|
|
271
|
+
try {
|
|
272
|
+
const json = schema instanceof z.ZodType ? z.toJSONSchema(schema, { io: "input", unrepresentable: "any" }) : schema;
|
|
273
|
+
return await llama.createGrammarForJsonSchema(stripSchemaMeta(json));
|
|
274
|
+
} catch {
|
|
275
|
+
return llama.getGrammarFor("json");
|
|
276
|
+
}
|
|
277
|
+
})();
|
|
278
|
+
grammars.set(schema, grammar);
|
|
279
|
+
}
|
|
280
|
+
return grammar;
|
|
281
|
+
}
|
|
282
|
+
function stripSchemaMeta(value) {
|
|
283
|
+
if (Array.isArray(value)) return value.map(stripSchemaMeta);
|
|
284
|
+
if (!value || typeof value !== "object") return value;
|
|
285
|
+
const out = {};
|
|
286
|
+
for (const [key, child] of Object.entries(value)) {
|
|
287
|
+
if (key === "$schema" || key === "default" || key === "additionalProperties") continue;
|
|
288
|
+
out[key] = stripSchemaMeta(child);
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
function withDeadline(promise, ms) {
|
|
293
|
+
return new Promise((resolve, reject) => {
|
|
294
|
+
const timer = setTimeout(
|
|
295
|
+
() => reject(Object.assign(new Error("deadline"), { name: "TimeoutError" })),
|
|
296
|
+
ms
|
|
297
|
+
);
|
|
298
|
+
promise.then(
|
|
299
|
+
(value) => {
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
resolve(value);
|
|
302
|
+
},
|
|
303
|
+
(error) => {
|
|
304
|
+
clearTimeout(timer);
|
|
305
|
+
reject(error);
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
var FOREGROUND_WINDOW_MS = 2e3;
|
|
311
|
+
var FOREGROUND_MAX_WAIT_MS = 15e3;
|
|
312
|
+
function foregroundPath(root) {
|
|
313
|
+
return join(root, ".codebuddy", "cache", "foreground");
|
|
314
|
+
}
|
|
315
|
+
async function markForeground(root) {
|
|
316
|
+
const path = foregroundPath(root);
|
|
317
|
+
const now = /* @__PURE__ */ new Date();
|
|
318
|
+
try {
|
|
319
|
+
await utimes(path, now, now);
|
|
320
|
+
} catch {
|
|
321
|
+
await mkdir(dirname(path), { recursive: true }).catch(() => {
|
|
322
|
+
});
|
|
323
|
+
await writeFile(path, "").catch(() => {
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function waitForForeground(root, signal) {
|
|
328
|
+
const until = Date.now() + FOREGROUND_MAX_WAIT_MS;
|
|
329
|
+
while (Date.now() < until && !signal?.aborted) {
|
|
330
|
+
const info = await stat(foregroundPath(root)).catch(() => null);
|
|
331
|
+
if (!info || Date.now() - info.mtimeMs > FOREGROUND_WINDOW_MS) return;
|
|
332
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async function localModelStatus(preferred) {
|
|
336
|
+
const installed = await installedLocalModel(preferred);
|
|
337
|
+
return {
|
|
338
|
+
runtime: await localRuntimeAvailable(),
|
|
339
|
+
installed,
|
|
340
|
+
path: installed ? localModelPath(installed) : null
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function formatBytes(bytes) {
|
|
344
|
+
return bytes >= GB ? `${(bytes / GB).toFixed(1)} GB` : `${Math.round(bytes / 1024 ** 2)} MB`;
|
|
345
|
+
}
|
|
346
|
+
function freeMemory() {
|
|
347
|
+
return freemem();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export {
|
|
351
|
+
LOCAL_MODELS,
|
|
352
|
+
LLAMA_RUNTIME,
|
|
353
|
+
localModelPath,
|
|
354
|
+
installedLocalModel,
|
|
355
|
+
localRuntimeAvailable,
|
|
356
|
+
machineCanCard,
|
|
357
|
+
checkLocalModelResources,
|
|
358
|
+
downloadLocalModel,
|
|
359
|
+
removeLocalModels,
|
|
360
|
+
generationThreads,
|
|
361
|
+
IDLE_UNLOAD_MS,
|
|
362
|
+
unloadLocalModel,
|
|
363
|
+
isLocalModelWarm,
|
|
364
|
+
warmLocalModel,
|
|
365
|
+
localChatJson,
|
|
366
|
+
markForeground,
|
|
367
|
+
waitForForeground,
|
|
368
|
+
localModelStatus,
|
|
369
|
+
formatBytes,
|
|
370
|
+
freeMemory
|
|
371
|
+
};
|
|
372
|
+
//# sourceMappingURL=chunk-NNJSAYHY.js.map
|