@lzhzzzzwill/cofos 1.3.4 → 1.3.8

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/README.md CHANGED
@@ -71,6 +71,20 @@ Disable RAG for a session:
71
71
  npx @lzhzzzzwill/cofos --no-rag
72
72
  ```
73
73
 
74
+ Show model reasoning scaffold when emitted:
75
+
76
+ ```bash
77
+ npx @lzhzzzzwill/cofos --show-reasoning
78
+ ```
79
+
80
+ Select the inference device explicitly:
81
+
82
+ ```bash
83
+ npx @lzhzzzzwill/cofos --device cuda # CUDA GPU machine
84
+ npx @lzhzzzzwill/cofos --device mps # Apple Silicon Mac
85
+ npx @lzhzzzzwill/cofos --device cpu # CPU fallback, slow
86
+ ```
87
+
74
88
  Ask once and exit, useful for scripts and smoke tests:
75
89
 
76
90
  ```bash
@@ -89,7 +103,8 @@ On startup, the CLI does the following:
89
103
  5. If `--pdf-dir` is provided, scans PDF names, sizes, and mtimes.
90
104
  6. Re-parses PDFs only when the folder contents changed or `--rebuild-pdf-index` is passed.
91
105
  7. Merges ROS-related PDF chunks into BM25 retrieval.
92
- 8. Loads the model once and keeps the Python backend alive for the whole chat session.
106
+ 8. Selects the inference backend: CUDA first, then Apple Silicon MPS, then CPU.
107
+ 9. Loads the model once and keeps the Python backend alive for the whole chat session.
93
108
 
94
109
  PDF files are read locally and are not uploaded anywhere. Set `COFOS_CACHE_DIR` to override the COFOS cache root, or `HF_HOME` to override only the Hugging Face model cache.
95
110
 
@@ -127,7 +142,10 @@ cofos \
127
142
  | `--cache-dir` | Folder for model, KG/BM25, and parsed PDF cache; defaults to `./.cofos` |
128
143
  | `--rebuild-pdf-index` | Force PDF parsing and BM25 rebuild |
129
144
  | `--top-k` | Retrieval top-k |
145
+ | `--device` | `auto`, `cuda`, `mps`, or `cpu`; defaults to `auto` |
146
+ | `--dtype` | `auto`, `float32`, `float16`, or `bfloat16`; defaults to `auto` |
130
147
  | `-p`, `--prompt` | Ask one question and exit |
148
+ | `--show-reasoning` | Show emitted reasoning scaffold in dim text before the final answer |
131
149
  | `--max-new-tokens` | Override generation length |
132
150
  | `--no-rag` | Run without KG/BM25 retrieval |
133
151
 
package/bin/cofos.js CHANGED
@@ -216,7 +216,7 @@ function createPlainUi() {
216
216
  async function main() {
217
217
  if (process.argv.includes("-h") || process.argv.includes("--help")) {
218
218
  console.log("\n COFOS CLI — 9B + RAG chat (Willlzh/COFOS)\n");
219
- console.log(" Usage: cofos [-p QUESTION] [--model PATH] [--config PATH] [--pdf-dir DIR] [--cache-dir DIR] [--top-k N] [--no-rag]\n");
219
+ console.log(" Usage: cofos [-p QUESTION] [--show-reasoning] [--device DEVICE] [--dtype DTYPE] [--model PATH] [--config PATH] [--pdf-dir DIR] [--cache-dir DIR] [--top-k N] [--no-rag]\n");
220
220
  console.log(" Commands in chat:");
221
221
  console.log(" /help Show this help");
222
222
  console.log(" /exit Quit");
@@ -229,6 +229,9 @@ async function main() {
229
229
  console.log(" --pdf-dir DIR Add PDFs from DIR as extra BM25 evidence");
230
230
  console.log(" --cache-dir DIR Store model/data cache here (default: ./.cofos)");
231
231
  console.log(" --max-new-tokens N Override generation length");
232
+ console.log(" --device DEVICE auto|cuda|mps|cpu (default: auto; CUDA first, then Mac MPS)");
233
+ console.log(" --dtype DTYPE auto|float32|float16|bfloat16 (default: auto)");
234
+ console.log(" --show-reasoning Show model reasoning scaffold in dim text when emitted");
232
235
  console.log(" -p, --prompt TEXT Ask once and exit");
233
236
  console.log(" --rebuild-pdf-index Force reparsing PDFs and rebuilding BM25\n");
234
237
  console.log(" Multi-line: end a line with \\ to continue on the next line.\n");
@@ -245,6 +248,9 @@ async function main() {
245
248
  let maxNewTokens = null;
246
249
  let cacheDir = process.env.COFOS_CACHE_DIR ? resolve(process.env.COFOS_CACHE_DIR) : resolve(process.cwd(), ".cofos");
247
250
  let rebuildPdfIndex = false;
251
+ let showReasoning = false;
252
+ let devicePreference = process.env.COFOS_DEVICE || "auto";
253
+ let dtypePreference = process.env.COFOS_DTYPE || "auto";
248
254
  let singlePrompt = null;
249
255
  const backendArgs = [chatScript];
250
256
 
@@ -276,6 +282,14 @@ async function main() {
276
282
  cacheDir = resolve(value(++i, arg));
277
283
  } else if (arg === "--rebuild-pdf-index") {
278
284
  rebuildPdfIndex = true;
285
+ } else if (arg === "--show-reasoning") {
286
+ showReasoning = true;
287
+ } else if (arg === "--device") {
288
+ devicePreference = value(++i, arg).toLowerCase();
289
+ if (!["auto", "cuda", "mps", "cpu", "gpu", "metal"].includes(devicePreference)) die(`--device must be one of auto, cuda, mps, cpu; got "${devicePreference}"`);
290
+ } else if (arg === "--dtype") {
291
+ dtypePreference = value(++i, arg).toLowerCase();
292
+ if (!["auto", "fp32", "float32", "fp16", "float16", "bf16", "bfloat16"].includes(dtypePreference)) die(`--dtype must be one of auto, float32, float16, bfloat16; got "${dtypePreference}"`);
279
293
  } else if (arg === "-p" || arg === "--prompt") {
280
294
  singlePrompt = value(++i, arg);
281
295
  } else if (arg === "--no-rag") {
@@ -311,6 +325,11 @@ async function main() {
311
325
  HF_HUB_ETAG_TIMEOUT: process.env.HF_HUB_ETAG_TIMEOUT || "30",
312
326
  HF_HUB_VERBOSITY: process.env.HF_HUB_VERBOSITY || "error",
313
327
  TRANSFORMERS_VERBOSITY: process.env.TRANSFORMERS_VERBOSITY || "error",
328
+ PYTHONUNBUFFERED: process.env.PYTHONUNBUFFERED || "1",
329
+ PYTHONIOENCODING: process.env.PYTHONIOENCODING || "utf-8",
330
+ COFOS_SHOW_REASONING: showReasoning ? "1" : (process.env.COFOS_SHOW_REASONING || "0"),
331
+ COFOS_DEVICE: devicePreference,
332
+ COFOS_DTYPE: dtypePreference,
314
333
  };
315
334
  const proc = spawn(py, backendArgs, {
316
335
  stdio: ["pipe", "pipe", "pipe"],
@@ -320,7 +339,7 @@ async function main() {
320
339
  proc.stdout.setEncoding("utf-8");
321
340
  proc.stderr.setEncoding("utf-8");
322
341
 
323
- let buf = "", ready = false, generating = false, responseOpen = false, commandOpen = false, closed = false;
342
+ let buf = "", ready = false, generating = false, responseOpen = false, commandOpen = false, closed = false, backendExited = false;
324
343
  let currentAnswer = "", assistantStart = -1, plainAssistantMode = "undecided", plainAssistantPending = "";
325
344
  let plainReasonBuffer = "", plainReasonPrinted = 0;
326
345
  const inputQ = [];
@@ -346,6 +365,37 @@ async function main() {
346
365
  }
347
366
  }
348
367
 
368
+ function handleLocalOnly(rawInput) {
369
+ const input = String(rawInput || "").trim();
370
+ if (!input) return true;
371
+ if (backendExited && !/^\/(exit|quit)$/.test(input)) {
372
+ ui.print("• COFOS backend is not running. Restart `cofos` after fixing the error above, or run /exit.");
373
+ return true;
374
+ }
375
+ if (input === "/") {
376
+ ui.print("• Type a command prefix such as /r or /to, then press Tab. Run /help for all commands.");
377
+ return true;
378
+ }
379
+ if (/^\/(exit|quit)$/.test(input)) { shutdown(); return true; }
380
+ if (/^\/(help)$/.test(input)) {
381
+ ui.print(slashHint("/") + "\n Add local papers to RAG:\n cofos --pdf-dir ./new_pdfs\n cofos --pdf-dir ./new_pdfs --rebuild-pdf-index\n\n Multi-line: end a line with \\ to continue.");
382
+ return true;
383
+ }
384
+ if (/^\/(cls)$/.test(input)) {
385
+ ui.clear();
386
+ return true;
387
+ }
388
+ if (/^\/(info)$/.test(input)) {
389
+ ui.print(`\n COFOS CLI — v${pkg.version}\n Model: ${modelPath} (Hugging Face)\n RAG: Willlzh/COFOS_data/runtime (KG + BM25) + optional --pdf-dir BM25 evidence\n Cache: ${cacheDir}\n HF: ${backendEnv.HF_HOME}\n Engine: StudentAdapterInference + transformers + PyTorch\n Device: ${devicePreference}\n Dtype: ${dtypePreference}\n Status: ${ready ? "ready" : "loading"}\n Hist: ~/.cofos_history.jsonl\n`);
390
+ return true;
391
+ }
392
+ if (input.startsWith("/") && !/^\/(clear|save|topk)(?:\s|$)/.test(input) && !/^\/rag\s+(?:on|off)$/.test(input)) {
393
+ ui.print(`{red-fg}•{/red-fg} Unknown command: ${input}.\n${slashPrefixHint(input)}`);
394
+ return true;
395
+ }
396
+ return false;
397
+ }
398
+
349
399
  function handle(rawInput) {
350
400
  let input = String(rawInput || "");
351
401
  if (input.endsWith("\\")) {
@@ -363,8 +413,12 @@ async function main() {
363
413
  input = input.trim();
364
414
  if (!input) return false;
365
415
 
416
+ if (backendExited && !/^\/(exit|quit)$/.test(input)) {
417
+ ui.print("• COFOS backend is not running. Restart `cofos` after fixing the error above, or run /exit.");
418
+ return true;
419
+ }
366
420
  if (input === "/") {
367
- ui.print("Commands: " + SLASH_COMMANDS.map(({ cmd }) => cmd).join(" ") + "\nType a prefix such as /r or /to, then press Tab for completion.");
421
+ ui.print(" Type a command prefix such as /r or /to, then press Tab. Run /help for all commands.");
368
422
  return false;
369
423
  }
370
424
  if (/^\/(exit|quit)$/.test(input)) { shutdown(); return true; }
@@ -405,7 +459,7 @@ async function main() {
405
459
  return true;
406
460
  }
407
461
  if (/^\/(info)$/.test(input)) {
408
- ui.print(`\n COFOS CLI — v${pkg.version}\n Model: ${modelPath} (Hugging Face)\n RAG: Willlzh/COFOS_data/runtime (KG + BM25) + optional --pdf-dir BM25 evidence\n Cache: ${cacheDir}\n HF: ${backendEnv.HF_HOME}\n Engine: StudentAdapterInference + transformers + PyTorch\n Hist: ~/.cofos_history.jsonl\n`);
462
+ ui.print(`\n COFOS CLI — v${pkg.version}\n Model: ${modelPath} (Hugging Face)\n RAG: Willlzh/COFOS_data/runtime (KG + BM25) + optional --pdf-dir BM25 evidence\n Cache: ${cacheDir}\n HF: ${backendEnv.HF_HOME}\n Engine: StudentAdapterInference + transformers + PyTorch\n Device: ${devicePreference}\n Dtype: ${dtypePreference}\n Hist: ~/.cofos_history.jsonl\n`);
409
463
  return false;
410
464
  }
411
465
  if (input.startsWith("/")) {
@@ -427,8 +481,19 @@ async function main() {
427
481
  }
428
482
 
429
483
  ui.setSubmitHandler((line) => {
484
+ if (handleLocalOnly(line)) {
485
+ if (!closed) ui.focus();
486
+ return;
487
+ }
488
+ if (backendExited) {
489
+ ui.print("• COFOS backend is not running. Restart `cofos` after fixing the error above, or run /exit.");
490
+ if (!closed) ui.focus();
491
+ return;
492
+ }
430
493
  if (!ready || generating) {
431
494
  inputQ.push(line);
495
+ ui.print(ready ? "• Queued until the current answer finishes." : "• Queued until the model is ready. Use /exit to quit while loading.");
496
+ if (!closed) ui.focus();
432
497
  return;
433
498
  }
434
499
  const started = handle(line);
@@ -476,12 +541,68 @@ async function main() {
476
541
 
477
542
  function streamPlainAssistant(text, final = false) {
478
543
  if (ui.supportsReplace) return;
544
+ if (!showReasoning) {
545
+ if (plainAssistantMode === "undecided") {
546
+ if (!text && !final) return;
547
+ plainAssistantMode = "answer";
548
+ ui.append(`${GR}${B}◆${R} `);
549
+ }
550
+ if (text) ui.append(text);
551
+ return;
552
+ }
553
+
554
+ plainAssistantPending += text;
479
555
  if (plainAssistantMode === "undecided") {
480
- if (!text && !final) return;
481
- plainAssistantMode = "answer";
482
- ui.append(`${GR}${B}◆${R} `);
556
+ const start = plainAssistantPending.trimStart();
557
+ if (!start && !final) return;
558
+ if (start.startsWith("<think>") || reasonRe.test(start)) {
559
+ plainAssistantMode = "reason";
560
+ plainReasonBuffer = start.replace(/^<think>\s*/i, "");
561
+ plainReasonPrinted = 0;
562
+ plainAssistantPending = "";
563
+ ui.append(`${G}-${R} ${G}`);
564
+ } else {
565
+ plainAssistantMode = "answer";
566
+ plainAssistantPending = start;
567
+ ui.append(`${GR}${B}◆${R} `);
568
+ }
569
+ }
570
+
571
+ if (plainAssistantMode === "reason") {
572
+ plainReasonBuffer += plainAssistantPending;
573
+ plainAssistantPending = "";
574
+ const thinkEnd = plainReasonBuffer.indexOf("</think>");
575
+ const paraEnd = plainReasonBuffer.search(/\n\s*\n/);
576
+ const split = thinkEnd !== -1 ? thinkEnd : paraEnd;
577
+ if (split !== -1) {
578
+ const reason = plainReasonBuffer.slice(plainReasonPrinted, split).replace(/<\/think>/gi, "");
579
+ const restStart = thinkEnd !== -1 ? split + "</think>".length : split;
580
+ const rest = plainReasonBuffer.slice(restStart).trimStart();
581
+ if (reason) ui.append(reason);
582
+ ui.append(`${R}\n${GR}${B}◆${R} `);
583
+ plainAssistantMode = "answer";
584
+ plainAssistantPending = rest;
585
+ plainReasonBuffer = "";
586
+ plainReasonPrinted = 0;
587
+ } else {
588
+ const printableUntil = final ? plainReasonBuffer.length : Math.max(0, plainReasonBuffer.length - 2);
589
+ if (printableUntil > plainReasonPrinted) {
590
+ ui.append(plainReasonBuffer.slice(plainReasonPrinted, printableUntil));
591
+ plainReasonPrinted = printableUntil;
592
+ }
593
+ if (!final) return;
594
+ if (plainReasonPrinted < plainReasonBuffer.length) ui.append(plainReasonBuffer.slice(plainReasonPrinted));
595
+ plainReasonBuffer = "";
596
+ plainReasonPrinted = 0;
597
+ ui.append(R);
598
+ return;
599
+ }
600
+ }
601
+
602
+ if (plainAssistantMode === "answer" && plainAssistantPending) {
603
+ ui.append(plainAssistantPending);
604
+ plainAssistantPending = "";
483
605
  }
484
- if (text) ui.append(text);
485
606
  }
486
607
 
487
608
 
@@ -562,8 +683,17 @@ async function main() {
562
683
  proc.on("error", (e) => { ui.print("[cofos] Python error: " + e.message); shutdown(); });
563
684
  proc.on("exit", (c) => {
564
685
  if (closed) return;
686
+ backendExited = true;
687
+ ready = false;
688
+ generating = false;
689
+ inputQ.length = 0;
565
690
  ui.print(c && c !== 0 ? "[cofos] Python exited with code " + c : "[cofos] Python backend exited.");
566
- ui.print("Press Ctrl+C to close.");
691
+ if (c && c !== 0) {
692
+ ui.print("[cofos] Backend failed before the model became ready. On Mac this usually means the 9B model could not fit in available memory and Transformers tried to offload the whole model to disk.");
693
+ ui.print("[cofos] Try a machine with more unified/GPU memory, force CPU with --device cpu for a slower attempt, or use a smaller/quantized model. Use /exit to close this session.");
694
+ } else {
695
+ ui.print("Press Ctrl+C to close.");
696
+ }
567
697
  ui.focus();
568
698
  process.exitCode = c ?? 0;
569
699
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzhzzzzwill/cofos",
3
- "version": "1.3.4",
3
+ "version": "1.3.8",
4
4
  "description": "COFOS 9B + RAG chat CLI with optional local PDF ingestion.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -225,14 +225,32 @@ class StudentAdapterInference:
225
225
  raise FileNotFoundError(f"Base model path not found: {self.model_path}")
226
226
 
227
227
  @staticmethod
228
- def _select_dtype() -> torch.dtype:
229
- """Pick a stable dtype for the current runtime.
228
+ def _select_device() -> str:
229
+ requested = os.environ.get("COFOS_DEVICE", "auto").strip().lower()
230
+ aliases = {"gpu": "cuda", "metal": "mps"}
231
+ requested = aliases.get(requested, requested)
232
+ if requested not in {"", "auto", "cuda", "mps", "cpu"}:
233
+ logging.warning("Ignoring unsupported COFOS_DEVICE=%s; using auto", requested)
234
+ requested = "auto"
235
+ if requested == "cuda":
236
+ if not torch.cuda.is_available():
237
+ raise RuntimeError("COFOS_DEVICE=cuda was requested, but CUDA is not available.")
238
+ return "cuda"
239
+ if requested == "mps":
240
+ if not getattr(torch.backends, "mps", None) or not torch.backends.mps.is_available():
241
+ raise RuntimeError("COFOS_DEVICE=mps was requested, but PyTorch MPS is not available.")
242
+ return "mps"
243
+ if requested == "cpu":
244
+ return "cpu"
245
+ if torch.cuda.is_available():
246
+ return "cuda"
247
+ if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
248
+ return "mps"
249
+ return "cpu"
230
250
 
231
- CUDA can safely use bf16/fp16. On Mac/CPU, ``device_map=auto`` may place
232
- layers on CPU or disk; fp16 there can produce NaN probabilities or
233
- garbage tokens. Prefer float32 outside CUDA unless the user explicitly
234
- opts into another dtype with COFOS_DTYPE.
235
- """
251
+ @staticmethod
252
+ def _select_dtype(device: str) -> torch.dtype:
253
+ """Pick a stable dtype for the selected runtime device."""
236
254
  requested = os.environ.get("COFOS_DTYPE", "auto").strip().lower()
237
255
  aliases = {
238
256
  "fp32": torch.float32,
@@ -247,10 +265,12 @@ class StudentAdapterInference:
247
265
  if requested not in {"", "auto"}:
248
266
  logging.warning("Ignoring unsupported COFOS_DTYPE=%s; using auto", requested)
249
267
 
250
- if torch.cuda.is_available():
268
+ if device == "cuda":
251
269
  if torch.cuda.is_bf16_supported():
252
270
  return torch.bfloat16
253
271
  return torch.float16
272
+ if device == "mps":
273
+ return torch.float16
254
274
  return torch.float32
255
275
 
256
276
  def load_model(self) -> None:
@@ -277,15 +297,30 @@ class StudentAdapterInference:
277
297
  # Decoder-only models must be left-padded for correct generation.
278
298
  self.tokenizer.padding_side = "left"
279
299
 
280
- dtype = self._select_dtype()
281
- logging.info("Loading model weights with dtype=%s", dtype)
282
- model = AutoModelForCausalLM.from_pretrained(
283
- self.model_path_str,
284
- device_map="auto",
285
- torch_dtype=dtype,
286
- low_cpu_mem_usage=True,
287
- trust_remote_code=True,
288
- )
300
+ device = self._select_device()
301
+ dtype = self._select_dtype(device)
302
+ logging.info("Loading model weights with dtype=%s on device=%s", dtype, device)
303
+ load_kwargs = {
304
+ "torch_dtype": dtype,
305
+ "low_cpu_mem_usage": True,
306
+ "trust_remote_code": True,
307
+ }
308
+ if device == "cuda":
309
+ load_kwargs["device_map"] = "auto"
310
+ try:
311
+ model = AutoModelForCausalLM.from_pretrained(self.model_path_str, **load_kwargs)
312
+ if device in {"mps", "cpu"}:
313
+ model = model.to(torch.device(device))
314
+ except ValueError as exc:
315
+ if "offload the whole model to the disk" in str(exc):
316
+ raise RuntimeError(
317
+ "COFOS could not fit the 9B model in available memory. "
318
+ "CUDA machines use automatic GPU placement; on Mac, COFOS avoids "
319
+ "Transformers disk offload and tries direct MPS loading instead. "
320
+ "Use a machine with more unified memory/GPU memory, or set "
321
+ "COFOS_DEVICE=cpu for a slower CPU-only attempt."
322
+ ) from exc
323
+ raise
289
324
  logging.info("Using merged model without a separate LoRA adapter")
290
325
 
291
326
  model.generation_config.use_cache = True
@@ -489,6 +524,11 @@ class StudentAdapterInference:
489
524
  @staticmethod
490
525
  def _sanitize_stream(chunks: Any) -> Generator[str, None, None]:
491
526
  """Drop ``<think>`` blocks and leading labels from a text stream."""
527
+ if os.environ.get("COFOS_SHOW_REASONING", "").lower() in {"1", "true", "yes", "on"}:
528
+ for chunk in chunks:
529
+ yield chunk
530
+ return
531
+
492
532
  buffer = ""
493
533
  started = False
494
534
  in_think = False