@lazarusrolando/crayoncli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -0
- package/README.md +22 -20
- package/dist/index.js +47 -38
- package/dist/providers/local-gemma.js +59 -89
- package/model/requirements.txt +6 -6
- package/model/server.py +96 -142
- package/package.json +7 -10
- package/src/index.ts +38 -47
- package/src/providers/local-gemma.ts +56 -143
- package/tsconfig.json +1 -15
- package/.npm-global/crayon +0 -16
- package/.npm-global/crayon.cmd +0 -17
- package/.npm-global/crayon.ps1 +0 -28
package/.env.example
CHANGED
package/README.md
CHANGED
|
@@ -1,38 +1,40 @@
|
|
|
1
|
-
# CRAYON v0.
|
|
1
|
+
# CRAYON v0.3.0 — Local Gemma Streaming
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Real local token streaming for the CRAYON CLI.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
## Flow
|
|
6
|
+
|
|
7
|
+
You → Node.js CLI → local FastAPI → Transformers TextIteratorStreamer → local Gemma → tokens → CLI
|
|
8
|
+
|
|
9
|
+
Hugging Face is only the model repository. Inference is local.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
13
|
```powershell
|
|
14
14
|
npm install
|
|
15
|
-
python -m pip install -r model
|
|
15
|
+
python -m pip install -r model\requirements.txt
|
|
16
16
|
Copy-Item .env.example .env
|
|
17
17
|
npm run build
|
|
18
|
-
npm link
|
|
19
18
|
```
|
|
20
19
|
|
|
21
20
|
## Run
|
|
22
21
|
|
|
23
22
|
```powershell
|
|
24
|
-
|
|
25
|
-
crayon "explain this project"
|
|
23
|
+
node dist/index.js
|
|
26
24
|
```
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
or:
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```env
|
|
33
|
-
CRAYON_MODEL=google/gemma-4-E2B-it
|
|
34
|
-
CRAYON_HOST=127.0.0.1
|
|
35
|
-
CRAYON_PORT=8765
|
|
36
|
-
CRAYON_PYTHON=python
|
|
37
|
-
CRAYON_MAX_NEW_TOKENS=1024
|
|
28
|
+
```powershell
|
|
29
|
+
node dist/index.js "Explain recursion in JavaScript"
|
|
38
30
|
```
|
|
31
|
+
|
|
32
|
+
## What v0.3 adds
|
|
33
|
+
|
|
34
|
+
- `TextIteratorStreamer` for local token streaming
|
|
35
|
+
- background generation thread
|
|
36
|
+
- FastAPI `StreamingResponse`
|
|
37
|
+
- Node.js `ReadableStream` consumption
|
|
38
|
+
- keeps `/generate` for non-streaming compatibility
|
|
39
|
+
|
|
40
|
+
Streaming changes delivery timing, not the inference provider. The model still runs locally.
|
package/dist/index.js
CHANGED
|
@@ -1,45 +1,54 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
import "dotenv/config";
|
|
3
|
-
import
|
|
4
|
-
import chalk from "chalk";
|
|
2
|
+
import readline from "node:readline";
|
|
5
3
|
import { LocalGemma } from "./providers/local-gemma.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.
|
|
11
|
-
.
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
process.
|
|
4
|
+
const gemma = new LocalGemma({
|
|
5
|
+
python: process.env.CRAYON_PYTHON || "python",
|
|
6
|
+
model: process.env.CRAYON_MODEL || "google/gemma-4-E2B-it",
|
|
7
|
+
host: process.env.CRAYON_HOST || "127.0.0.1",
|
|
8
|
+
port: Number(process.env.CRAYON_PORT || 8765),
|
|
9
|
+
maxNewTokens: Number(process.env.CRAYON_MAX_NEW_TOKENS || 1024),
|
|
10
|
+
temperature: Number(process.env.CRAYON_TEMPERATURE || 0.7)
|
|
11
|
+
});
|
|
12
|
+
async function main() {
|
|
13
|
+
const oneShot = process.argv.slice(2).join(" ").trim();
|
|
14
|
+
await gemma.start();
|
|
15
|
+
if (oneShot) {
|
|
16
|
+
await gemma.generateStream(oneShot);
|
|
16
17
|
gemma.stop();
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
console.log(
|
|
20
|
-
console.log(
|
|
21
|
-
console.log(
|
|
22
|
-
console.log(
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log("\n==========================================");
|
|
21
|
+
console.log(" CRAYON v0.3.0 - LOCAL STREAMING");
|
|
22
|
+
console.log("==========================================");
|
|
23
|
+
console.log("Gemma inference: LOCAL");
|
|
24
|
+
console.log("Hugging Face inference API: NOT USED");
|
|
25
|
+
console.log("Streaming: ENABLED\n");
|
|
26
|
+
console.log("Type /exit to quit.\n");
|
|
27
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "You: " });
|
|
28
|
+
const shutdown = () => { rl.close(); gemma.stop(); process.exit(0); };
|
|
29
|
+
process.on("SIGINT", shutdown);
|
|
30
|
+
rl.prompt();
|
|
31
|
+
rl.on("line", async (line) => {
|
|
32
|
+
const prompt = line.trim();
|
|
33
|
+
if (!prompt) {
|
|
34
|
+
rl.prompt();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (prompt === "/exit") {
|
|
38
|
+
shutdown();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
rl.pause();
|
|
25
42
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
console.log(chalk.dim("Thinking...\n"));
|
|
31
|
-
console.log(await gemma.generate(q));
|
|
32
|
-
console.log();
|
|
33
|
-
}
|
|
43
|
+
await gemma.generateStream(prompt);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error("\n[CRAYON ERROR]", error instanceof Error ? error.message : error);
|
|
34
47
|
}
|
|
35
48
|
finally {
|
|
36
|
-
rl.
|
|
37
|
-
|
|
49
|
+
rl.resume();
|
|
50
|
+
rl.prompt();
|
|
38
51
|
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
gemma.stop();
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
await program.parseAsync();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
main().catch(error => { console.error("[CRAYON FATAL]", error); gemma.stop(); process.exit(1); });
|
|
@@ -1,109 +1,79 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
3
2
|
export class LocalGemma {
|
|
4
3
|
process;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
this.python = options.python ?? process.env.CRAYON_PYTHON ?? "python";
|
|
11
|
-
this.host = options.host ?? process.env.CRAYON_HOST ?? "127.0.0.1";
|
|
12
|
-
this.port = options.port ?? Number(process.env.CRAYON_PORT ?? 8765);
|
|
13
|
-
this.model =
|
|
14
|
-
options.model ??
|
|
15
|
-
process.env.CRAYON_MODEL ??
|
|
16
|
-
"google/gemma-4-E2B-it";
|
|
17
|
-
}
|
|
18
|
-
url(path) {
|
|
19
|
-
return `http://${this.host}:${this.port}${path}`;
|
|
4
|
+
options;
|
|
5
|
+
baseUrl;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
this.baseUrl = `http://${options.host}:${options.port}`;
|
|
20
9
|
}
|
|
21
10
|
async start() {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
11
|
+
if (await this.isReady())
|
|
12
|
+
return;
|
|
13
|
+
console.log("[CRAYON] Starting local Gemma server...");
|
|
14
|
+
console.log(`[CRAYON] Model: ${this.options.model}`);
|
|
15
|
+
this.process = spawn(this.options.python, ["model/server.py"], {
|
|
16
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
17
|
+
env: { ...process.env, CRAYON_MODEL: this.options.model, CRAYON_HOST: this.options.host,
|
|
18
|
+
CRAYON_PORT: String(this.options.port) }
|
|
19
|
+
});
|
|
20
|
+
this.process.stdout?.on("data", (d) => process.stdout.write(`[Gemma] ${d.toString()}`));
|
|
21
|
+
this.process.stderr?.on("data", (d) => process.stderr.write(`[Gemma] ${d.toString()}`));
|
|
22
|
+
const deadline = Date.now() + 180000;
|
|
23
|
+
while (Date.now() < deadline) {
|
|
24
|
+
if (await this.isReady()) {
|
|
25
|
+
console.log("[CRAYON] Local Gemma server is ready.");
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
29
|
+
}
|
|
30
|
+
throw new Error("Gemma server did not become ready within 180 seconds.");
|
|
31
|
+
}
|
|
32
|
+
async isReady() {
|
|
33
|
+
try {
|
|
34
|
+
return (await fetch(`${this.baseUrl}/health`)).ok;
|
|
28
35
|
}
|
|
29
36
|
catch {
|
|
30
|
-
|
|
37
|
+
return false;
|
|
31
38
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
...process.env,
|
|
38
|
-
CRAYON_MODEL: this.model,
|
|
39
|
-
CRAYON_HOST: this.host,
|
|
40
|
-
CRAYON_PORT: String(this.port),
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
// stdout is available because we configured it as "pipe".
|
|
44
|
-
this.process.stdout?.on("data", (data) => {
|
|
45
|
-
process.stdout.write(`\x1b[90m[Gemma] ${data.toString()}\x1b[0m`);
|
|
46
|
-
});
|
|
47
|
-
// stderr is available because we configured it as "pipe".
|
|
48
|
-
this.process.stderr?.on("data", (data) => {
|
|
49
|
-
process.stderr.write(`\x1b[90m[Gemma] ${data.toString()}\x1b[0m`);
|
|
50
|
-
});
|
|
51
|
-
this.process.on("error", (error) => {
|
|
52
|
-
console.error("[CRAYON] Failed to start Gemma:", error);
|
|
53
|
-
});
|
|
54
|
-
this.process.on("exit", (code, signal) => {
|
|
55
|
-
if (code !== 0 && code !== null) {
|
|
56
|
-
console.error(`[CRAYON] Gemma server exited with code ${code}`);
|
|
57
|
-
}
|
|
58
|
-
if (signal) {
|
|
59
|
-
console.error(`[CRAYON] Gemma server stopped by signal ${signal}`);
|
|
60
|
-
}
|
|
39
|
+
}
|
|
40
|
+
async generateStream(prompt) {
|
|
41
|
+
const response = await fetch(`${this.baseUrl}/generate/stream`, {
|
|
42
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
43
|
+
body: JSON.stringify({ prompt, max_new_tokens: this.options.maxNewTokens, temperature: this.options.temperature })
|
|
61
44
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
45
|
+
if (!response.ok)
|
|
46
|
+
throw new Error(`Gemma streaming error (${response.status}): ${await response.text()}`);
|
|
47
|
+
if (!response.body)
|
|
48
|
+
throw new Error("Gemma returned no streaming body.");
|
|
49
|
+
const reader = response.body.getReader();
|
|
50
|
+
const decoder = new TextDecoder();
|
|
51
|
+
process.stdout.write("\nCRAYON: ");
|
|
52
|
+
try {
|
|
53
|
+
while (true) {
|
|
54
|
+
const { done, value } = await reader.read();
|
|
55
|
+
if (done)
|
|
56
|
+
break;
|
|
57
|
+
process.stdout.write(decoder.decode(value, { stream: true }));
|
|
73
58
|
}
|
|
74
|
-
|
|
59
|
+
process.stdout.write(decoder.decode());
|
|
60
|
+
process.stdout.write("\n");
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
reader.releaseLock();
|
|
75
64
|
}
|
|
76
|
-
this.stop();
|
|
77
|
-
throw new Error("Gemma server did not become ready within 180 seconds.");
|
|
78
65
|
}
|
|
79
|
-
async generate(prompt
|
|
80
|
-
await this.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
headers: {
|
|
84
|
-
"content-type": "application/json",
|
|
85
|
-
},
|
|
86
|
-
body: JSON.stringify({
|
|
87
|
-
prompt,
|
|
88
|
-
max_new_tokens: maxNewTokens,
|
|
89
|
-
temperature: 0.2,
|
|
90
|
-
top_p: 0.9,
|
|
91
|
-
}),
|
|
66
|
+
async generate(prompt) {
|
|
67
|
+
const response = await fetch(`${this.baseUrl}/generate`, {
|
|
68
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
69
|
+
body: JSON.stringify({ prompt, max_new_tokens: this.options.maxNewTokens, temperature: this.options.temperature })
|
|
92
70
|
});
|
|
93
|
-
if (!response.ok)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
const result = (await response.json());
|
|
98
|
-
return result.text;
|
|
71
|
+
if (!response.ok)
|
|
72
|
+
throw new Error(`Gemma error (${response.status}): ${await response.text()}`);
|
|
73
|
+
return (await response.json()).text;
|
|
99
74
|
}
|
|
100
75
|
stop() {
|
|
101
|
-
if (!this.process)
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (!this.process.killed) {
|
|
76
|
+
if (this.process && !this.process.killed)
|
|
105
77
|
this.process.kill();
|
|
106
|
-
}
|
|
107
|
-
this.process = undefined;
|
|
108
78
|
}
|
|
109
79
|
}
|
package/model/requirements.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
fastapi
|
|
2
|
+
uvicorn[standard]
|
|
3
|
+
transformers
|
|
4
|
+
torch
|
|
5
|
+
accelerate
|
|
6
|
+
sentencepiece
|
package/model/server.py
CHANGED
|
@@ -1,159 +1,113 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from threading import Thread
|
|
2
3
|
import torch
|
|
3
|
-
from fastapi import FastAPI
|
|
4
|
+
from fastapi import FastAPI
|
|
5
|
+
from fastapi.responses import StreamingResponse
|
|
4
6
|
from pydantic import BaseModel
|
|
5
|
-
from transformers import AutoTokenizer,
|
|
7
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
|
|
6
8
|
|
|
7
|
-
MODEL_ID
|
|
8
|
-
HOST
|
|
9
|
-
PORT
|
|
10
|
-
|
|
11
|
-
print("=" * 60)
|
|
12
|
-
print("[CRAYON] Local Gemma Server")
|
|
13
|
-
print("=" * 60)
|
|
14
|
-
print(f"[CRAYON] Model: {MODEL_ID}")
|
|
15
|
-
print(f"[CRAYON] PyTorch: {torch.__version__}")
|
|
16
|
-
print(f"[CRAYON] CUDA available: {torch.cuda.is_available()}")
|
|
17
|
-
|
|
18
|
-
if torch.cuda.is_available():
|
|
19
|
-
print(f"[CRAYON] GPU: {torch.cuda.get_device_name(0)}")
|
|
20
|
-
print(f"[CRAYON] CUDA: {torch.version.cuda}")
|
|
21
|
-
|
|
22
|
-
print("[CRAYON] Loading tokenizer...")
|
|
23
|
-
|
|
24
|
-
tokenizer = AutoTokenizer.from_pretrained(
|
|
25
|
-
MODEL_ID
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
print("[CRAYON] Tokenizer loaded.")
|
|
29
|
-
print("[CRAYON] Loading model...")
|
|
30
|
-
|
|
31
|
-
model = AutoModelForCausalLM.from_pretrained(
|
|
32
|
-
MODEL_ID,
|
|
33
|
-
device_map="auto",
|
|
34
|
-
torch_dtype="auto",
|
|
35
|
-
)
|
|
9
|
+
MODEL_ID=os.getenv("CRAYON_MODEL","google/gemma-4-E2B-it")
|
|
10
|
+
HOST=os.getenv("CRAYON_HOST","127.0.0.1")
|
|
11
|
+
PORT=int(os.getenv("CRAYON_PORT","8765"))
|
|
36
12
|
|
|
13
|
+
print(f"[CRAYON MODEL] Loading: {MODEL_ID}",flush=True)
|
|
14
|
+
print(f"[CRAYON MODEL] CUDA available: {torch.cuda.is_available()}",flush=True)
|
|
15
|
+
tokenizer=AutoTokenizer.from_pretrained(MODEL_ID)
|
|
16
|
+
model=AutoModelForCausalLM.from_pretrained(MODEL_ID,device_map="auto",torch_dtype="auto")
|
|
37
17
|
model.eval()
|
|
18
|
+
print("[CRAYON MODEL] Model loaded.",flush=True)
|
|
19
|
+
print(f"[CRAYON MODEL] Device: {model.device}",flush=True)
|
|
38
20
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
app = FastAPI(
|
|
42
|
-
title="CRAYON Local Gemma Server",
|
|
43
|
-
version="0.2.1",
|
|
44
|
-
)
|
|
45
|
-
|
|
21
|
+
app=FastAPI(title="CRAYON Local Gemma Server",version="0.3.0")
|
|
46
22
|
|
|
47
23
|
class GenerateRequest(BaseModel):
|
|
48
|
-
prompt:
|
|
49
|
-
max_new_tokens:
|
|
50
|
-
temperature:
|
|
51
|
-
top_p: float = 0.9
|
|
52
|
-
|
|
24
|
+
prompt:str
|
|
25
|
+
max_new_tokens:int=1024
|
|
26
|
+
temperature:float=0.7
|
|
53
27
|
|
|
54
28
|
@app.get("/health")
|
|
55
29
|
def health():
|
|
56
|
-
return {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
print("[CRAYON] Generating...")
|
|
113
|
-
|
|
114
|
-
with torch.inference_mode():
|
|
115
|
-
|
|
116
|
-
output = model.generate(
|
|
117
|
-
**generation_kwargs
|
|
30
|
+
return {"status":"ok","model":MODEL_ID,"device":str(model.device),
|
|
31
|
+
"cuda":torch.cuda.is_available(),"streaming":True,"inference":"local"}
|
|
32
|
+
|
|
33
|
+
def build_inputs(prompt):
|
|
34
|
+
messages=[{"role":"user","content":prompt}]
|
|
35
|
+
inputs=tokenizer.apply_chat_template(messages,tokenize=True,add_generation_prompt=True,
|
|
36
|
+
return_tensors="pt",return_dict=True)
|
|
37
|
+
input_ids=inputs["input_ids"].to(model.device)
|
|
38
|
+
attention_mask=inputs.get("attention_mask")
|
|
39
|
+
if attention_mask is not None:
|
|
40
|
+
attention_mask=attention_mask.to(model.device)
|
|
41
|
+
return input_ids,attention_mask
|
|
42
|
+
|
|
43
|
+
@app.post("/generate/stream")
|
|
44
|
+
def generate_stream(request:GenerateRequest):
|
|
45
|
+
input_ids,attention_mask=build_inputs(request.prompt)
|
|
46
|
+
streamer=TextIteratorStreamer(tokenizer,skip_prompt=True,skip_special_tokens=True)
|
|
47
|
+
kwargs={"input_ids":input_ids,"streamer":streamer,
|
|
48
|
+
"max_new_tokens":request.max_new_tokens,
|
|
49
|
+
"do_sample":request.temperature>0}
|
|
50
|
+
if attention_mask is not None: kwargs["attention_mask"]=attention_mask
|
|
51
|
+
if request.temperature>0: kwargs["temperature"]=request.temperature
|
|
52
|
+
|
|
53
|
+
worker=Thread(target=model.generate,kwargs=kwargs,daemon=True)
|
|
54
|
+
worker.start()
|
|
55
|
+
|
|
56
|
+
def token_iterator():
|
|
57
|
+
generated_text = ""
|
|
58
|
+
chunk_count = 0
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
for text in streamer:
|
|
62
|
+
chunk_count += 1
|
|
63
|
+
generated_text += text
|
|
64
|
+
|
|
65
|
+
# TextIteratorStreamer emits decoded text chunks. Re-tokenize
|
|
66
|
+
# accumulated output to report the generated-token count.
|
|
67
|
+
token_count = len(
|
|
68
|
+
tokenizer.encode(
|
|
69
|
+
generated_text,
|
|
70
|
+
add_special_tokens=False,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
print(
|
|
75
|
+
f"[CRAYON TOKEN] #{token_count}: {text!r}",
|
|
76
|
+
flush=True,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
yield text
|
|
80
|
+
|
|
81
|
+
final_token_count = len(
|
|
82
|
+
tokenizer.encode(
|
|
83
|
+
generated_text,
|
|
84
|
+
add_special_tokens=False,
|
|
85
|
+
)
|
|
118
86
|
)
|
|
119
87
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
skip_special_tokens=True,
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
print(
|
|
132
|
-
f"[CRAYON] Generated {len(generated_tokens)} tokens."
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
"text": text,
|
|
137
|
-
"model": MODEL_ID,
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
except Exception as exc:
|
|
141
|
-
|
|
142
|
-
print("[CRAYON] Generation error:")
|
|
143
|
-
print(repr(exc))
|
|
144
|
-
|
|
145
|
-
raise HTTPException(
|
|
146
|
-
status_code=500,
|
|
147
|
-
detail=str(exc),
|
|
148
|
-
)
|
|
149
|
-
|
|
88
|
+
print(
|
|
89
|
+
f"[CRAYON STREAM] Generation complete | "
|
|
90
|
+
f"chunks={chunk_count} | "
|
|
91
|
+
f"generated_tokens={final_token_count}",
|
|
92
|
+
flush=True,
|
|
93
|
+
)
|
|
94
|
+
finally:
|
|
95
|
+
worker.join()
|
|
150
96
|
|
|
151
|
-
|
|
97
|
+
return StreamingResponse(token_iterator(),media_type="text/plain; charset=utf-8",
|
|
98
|
+
headers={"Cache-Control":"no-cache","X-CRAYON-Streaming":"true"})
|
|
152
99
|
|
|
100
|
+
@app.post("/generate")
|
|
101
|
+
def generate(request:GenerateRequest):
|
|
102
|
+
input_ids,attention_mask=build_inputs(request.prompt)
|
|
103
|
+
kwargs={"input_ids":input_ids,"max_new_tokens":request.max_new_tokens,
|
|
104
|
+
"do_sample":request.temperature>0}
|
|
105
|
+
if attention_mask is not None: kwargs["attention_mask"]=attention_mask
|
|
106
|
+
if request.temperature>0: kwargs["temperature"]=request.temperature
|
|
107
|
+
output=model.generate(**kwargs)
|
|
108
|
+
new_tokens=output[0][input_ids.shape[-1]:]
|
|
109
|
+
return {"text":tokenizer.decode(new_tokens,skip_special_tokens=True)}
|
|
110
|
+
|
|
111
|
+
if __name__=="__main__":
|
|
153
112
|
import uvicorn
|
|
154
|
-
|
|
155
|
-
uvicorn.run(
|
|
156
|
-
app,
|
|
157
|
-
host=HOST,
|
|
158
|
-
port=PORT,
|
|
159
|
-
)
|
|
113
|
+
uvicorn.run(app,host=HOST,port=PORT)
|
package/package.json
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazarusrolando/crayoncli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"bin": {
|
|
6
|
-
"crayon": "dist/index.js"
|
|
7
|
-
},
|
|
8
5
|
"scripts": {
|
|
9
6
|
"build": "tsc",
|
|
10
|
-
"start": "node dist/index.js"
|
|
7
|
+
"start": "node dist/index.js",
|
|
8
|
+
"dev": "tsx src/index.ts"
|
|
11
9
|
},
|
|
12
10
|
"dependencies": {
|
|
13
|
-
"
|
|
14
|
-
"commander": "^14.0.1",
|
|
15
|
-
"dotenv": "^17.2.1"
|
|
11
|
+
"dotenv": "^16.4.7"
|
|
16
12
|
},
|
|
17
13
|
"devDependencies": {
|
|
18
|
-
"@types/node": "^
|
|
19
|
-
"
|
|
14
|
+
"@types/node": "^22.10.5",
|
|
15
|
+
"tsx": "^4.19.2",
|
|
16
|
+
"typescript": "^5.7.2"
|
|
20
17
|
}
|
|
21
18
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,52 +1,43 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
import "dotenv/config";
|
|
3
|
-
import
|
|
4
|
-
import chalk from "chalk";
|
|
2
|
+
import readline from "node:readline";
|
|
5
3
|
import { LocalGemma } from "./providers/local-gemma.js";
|
|
6
|
-
import { createInterface } from "node:readline/promises";
|
|
7
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
8
4
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
program.action(async (task?: string) => {
|
|
17
|
-
const gemma = new LocalGemma();
|
|
18
|
-
|
|
19
|
-
process.on("SIGINT", () => {
|
|
20
|
-
gemma.stop();
|
|
21
|
-
process.exit(130);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
console.log(chalk.cyan("\n╭──────────────────────────────────────────────╮"));
|
|
25
|
-
console.log(chalk.cyan("│ CRAYON LOCAL CODER │"));
|
|
26
|
-
console.log(chalk.cyan("│ Google Gemma · Local Inference │"));
|
|
27
|
-
console.log(chalk.cyan("╰──────────────────────────────────────────────╯\n"));
|
|
28
|
-
|
|
29
|
-
if (!task) {
|
|
30
|
-
const rl = createInterface({input, output});
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
while (true) {
|
|
34
|
-
const q = (await rl.question(chalk.green("> "))).trim();
|
|
35
|
-
|
|
36
|
-
if (!q || q === "exit" || q === "quit") break;
|
|
37
|
-
|
|
38
|
-
console.log(chalk.dim("Thinking...\n"));
|
|
39
|
-
console.log(await gemma.generate(q));
|
|
40
|
-
console.log();
|
|
41
|
-
}
|
|
42
|
-
} finally {
|
|
43
|
-
rl.close();
|
|
44
|
-
gemma.stop();
|
|
45
|
-
}
|
|
46
|
-
} else {
|
|
47
|
-
console.log(await gemma.generate(task));
|
|
48
|
-
gemma.stop();
|
|
49
|
-
}
|
|
5
|
+
const gemma=new LocalGemma({
|
|
6
|
+
python:process.env.CRAYON_PYTHON||"python",
|
|
7
|
+
model:process.env.CRAYON_MODEL||"google/gemma-4-E2B-it",
|
|
8
|
+
host:process.env.CRAYON_HOST||"127.0.0.1",
|
|
9
|
+
port:Number(process.env.CRAYON_PORT||8765),
|
|
10
|
+
maxNewTokens:Number(process.env.CRAYON_MAX_NEW_TOKENS||1024),
|
|
11
|
+
temperature:Number(process.env.CRAYON_TEMPERATURE||0.7)
|
|
50
12
|
});
|
|
51
13
|
|
|
52
|
-
|
|
14
|
+
async function main(){
|
|
15
|
+
const oneShot=process.argv.slice(2).join(" ").trim();
|
|
16
|
+
await gemma.start();
|
|
17
|
+
|
|
18
|
+
if(oneShot){ await gemma.generateStream(oneShot); gemma.stop(); return; }
|
|
19
|
+
|
|
20
|
+
console.log("\n==========================================");
|
|
21
|
+
console.log(" CRAYON v0.3.0 - LOCAL STREAMING");
|
|
22
|
+
console.log("==========================================");
|
|
23
|
+
console.log("Gemma inference: LOCAL");
|
|
24
|
+
console.log("Hugging Face inference API: NOT USED");
|
|
25
|
+
console.log("Streaming: ENABLED\n");
|
|
26
|
+
console.log("Type /exit to quit.\n");
|
|
27
|
+
|
|
28
|
+
const rl=readline.createInterface({input:process.stdin,output:process.stdout,prompt:"You: "});
|
|
29
|
+
const shutdown=()=>{rl.close();gemma.stop();process.exit(0);};
|
|
30
|
+
process.on("SIGINT",shutdown);
|
|
31
|
+
rl.prompt();
|
|
32
|
+
|
|
33
|
+
rl.on("line",async line=>{
|
|
34
|
+
const prompt=line.trim();
|
|
35
|
+
if(!prompt){rl.prompt();return;}
|
|
36
|
+
if(prompt==="/exit"){shutdown();return;}
|
|
37
|
+
rl.pause();
|
|
38
|
+
try{await gemma.generateStream(prompt);}
|
|
39
|
+
catch(error){console.error("\n[CRAYON ERROR]",error instanceof Error?error.message:error);}
|
|
40
|
+
finally{rl.resume();rl.prompt();}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
main().catch(error=>{console.error("[CRAYON FATAL]",error);gemma.stop();process.exit(1);});
|
|
@@ -1,165 +1,78 @@
|
|
|
1
|
-
import { spawn,
|
|
2
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
1
|
+
import { spawn, ChildProcess } from "node:child_process";
|
|
3
2
|
|
|
4
3
|
export interface GemmaOptions {
|
|
5
|
-
python
|
|
6
|
-
|
|
7
|
-
port?: number;
|
|
8
|
-
model?: string;
|
|
4
|
+
python:string; model:string; host:string; port:number;
|
|
5
|
+
maxNewTokens:number; temperature:number;
|
|
9
6
|
}
|
|
10
7
|
|
|
11
8
|
export class LocalGemma {
|
|
12
|
-
private process?:
|
|
9
|
+
private process?:ChildProcess;
|
|
10
|
+
private readonly options:GemmaOptions;
|
|
11
|
+
private readonly baseUrl:string;
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
private readonly model: string;
|
|
18
|
-
|
|
19
|
-
constructor(options: GemmaOptions = {}) {
|
|
20
|
-
this.python = options.python ?? process.env.CRAYON_PYTHON ?? "python";
|
|
21
|
-
this.host = options.host ?? process.env.CRAYON_HOST ?? "127.0.0.1";
|
|
22
|
-
this.port = options.port ?? Number(process.env.CRAYON_PORT ?? 8765);
|
|
23
|
-
this.model =
|
|
24
|
-
options.model ??
|
|
25
|
-
process.env.CRAYON_MODEL ??
|
|
26
|
-
"google/gemma-4-E2B-it";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
private url(path: string): string {
|
|
30
|
-
return `http://${this.host}:${this.port}${path}`;
|
|
13
|
+
constructor(options:GemmaOptions){
|
|
14
|
+
this.options=options;
|
|
15
|
+
this.baseUrl=`http://${options.host}:${options.port}`;
|
|
31
16
|
}
|
|
32
17
|
|
|
33
|
-
async start():
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (response.ok) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
} catch {
|
|
42
|
-
// Server isn't running; start it below.
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
console.log(`[CRAYON] Starting local Gemma server...`);
|
|
46
|
-
console.log(`[CRAYON] Model: ${this.model}`);
|
|
47
|
-
|
|
48
|
-
this.process = spawn(
|
|
49
|
-
this.python,
|
|
50
|
-
["model/server.py"],
|
|
51
|
-
{
|
|
52
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
53
|
-
env: {
|
|
54
|
-
...process.env,
|
|
55
|
-
CRAYON_MODEL: this.model,
|
|
56
|
-
CRAYON_HOST: this.host,
|
|
57
|
-
CRAYON_PORT: String(this.port),
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
);
|
|
18
|
+
async start():Promise<void>{
|
|
19
|
+
if(await this.isReady()) return;
|
|
20
|
+
console.log("[CRAYON] Starting local Gemma server...");
|
|
21
|
+
console.log(`[CRAYON] Model: ${this.options.model}`);
|
|
61
22
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
process.
|
|
65
|
-
|
|
66
|
-
);
|
|
23
|
+
this.process=spawn(this.options.python,["model/server.py"],{
|
|
24
|
+
stdio:["ignore","pipe","pipe"],
|
|
25
|
+
env:{...process.env,CRAYON_MODEL:this.options.model,CRAYON_HOST:this.options.host,
|
|
26
|
+
CRAYON_PORT:String(this.options.port)}
|
|
67
27
|
});
|
|
68
28
|
|
|
69
|
-
|
|
70
|
-
this.process.stderr?.on("data",
|
|
71
|
-
process.stderr.write(
|
|
72
|
-
`\x1b[90m[Gemma] ${data.toString()}\x1b[0m`,
|
|
73
|
-
);
|
|
74
|
-
});
|
|
29
|
+
this.process.stdout?.on("data",(d:Buffer)=>process.stdout.write(`[Gemma] ${d.toString()}`));
|
|
30
|
+
this.process.stderr?.on("data",(d:Buffer)=>process.stderr.write(`[Gemma] ${d.toString()}`));
|
|
75
31
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
32
|
+
const deadline=Date.now()+180000;
|
|
33
|
+
while(Date.now()<deadline){
|
|
34
|
+
if(await this.isReady()){ console.log("[CRAYON] Local Gemma server is ready."); return; }
|
|
35
|
+
await new Promise(r=>setTimeout(r,1000));
|
|
36
|
+
}
|
|
37
|
+
throw new Error("Gemma server did not become ready within 180 seconds.");
|
|
38
|
+
}
|
|
79
39
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`[CRAYON] Gemma server exited with code ${code}`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
40
|
+
async isReady():Promise<boolean>{
|
|
41
|
+
try{return (await fetch(`${this.baseUrl}/health`)).ok;}catch{return false;}
|
|
42
|
+
}
|
|
86
43
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
44
|
+
async generateStream(prompt:string):Promise<void>{
|
|
45
|
+
const response=await fetch(`${this.baseUrl}/generate/stream`,{
|
|
46
|
+
method:"POST",headers:{"Content-Type":"application/json"},
|
|
47
|
+
body:JSON.stringify({prompt,max_new_tokens:this.options.maxNewTokens,temperature:this.options.temperature})
|
|
92
48
|
});
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// Still starting.
|
|
49
|
+
if(!response.ok) throw new Error(`Gemma streaming error (${response.status}): ${await response.text()}`);
|
|
50
|
+
if(!response.body) throw new Error("Gemma returned no streaming body.");
|
|
51
|
+
|
|
52
|
+
const reader=response.body.getReader();
|
|
53
|
+
const decoder=new TextDecoder();
|
|
54
|
+
process.stdout.write("\nCRAYON: ");
|
|
55
|
+
try{
|
|
56
|
+
while(true){
|
|
57
|
+
const {done,value}=await reader.read();
|
|
58
|
+
if(done) break;
|
|
59
|
+
process.stdout.write(decoder.decode(value,{stream:true}));
|
|
105
60
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
this.stop();
|
|
111
|
-
|
|
112
|
-
throw new Error(
|
|
113
|
-
"Gemma server did not become ready within 180 seconds.",
|
|
114
|
-
);
|
|
61
|
+
process.stdout.write(decoder.decode());
|
|
62
|
+
process.stdout.write("\n");
|
|
63
|
+
}finally{reader.releaseLock();}
|
|
115
64
|
}
|
|
116
65
|
|
|
117
|
-
async generate(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
),
|
|
122
|
-
): Promise<string> {
|
|
123
|
-
await this.start();
|
|
124
|
-
|
|
125
|
-
const response = await fetch(this.url("/generate"), {
|
|
126
|
-
method: "POST",
|
|
127
|
-
headers: {
|
|
128
|
-
"content-type": "application/json",
|
|
129
|
-
},
|
|
130
|
-
body: JSON.stringify({
|
|
131
|
-
prompt,
|
|
132
|
-
max_new_tokens: maxNewTokens,
|
|
133
|
-
temperature: 0.2,
|
|
134
|
-
top_p: 0.9,
|
|
135
|
-
}),
|
|
66
|
+
async generate(prompt:string):Promise<string>{
|
|
67
|
+
const response=await fetch(`${this.baseUrl}/generate`,{
|
|
68
|
+
method:"POST",headers:{"Content-Type":"application/json"},
|
|
69
|
+
body:JSON.stringify({prompt,max_new_tokens:this.options.maxNewTokens,temperature:this.options.temperature})
|
|
136
70
|
});
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const errorText = await response.text();
|
|
140
|
-
|
|
141
|
-
throw new Error(
|
|
142
|
-
`Gemma server returned HTTP ${response.status}: ${errorText}`,
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const result = (await response.json()) as {
|
|
147
|
-
text: string;
|
|
148
|
-
model?: string;
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
return result.text;
|
|
71
|
+
if(!response.ok) throw new Error(`Gemma error (${response.status}): ${await response.text()}`);
|
|
72
|
+
return ((await response.json()) as {text:string}).text;
|
|
152
73
|
}
|
|
153
74
|
|
|
154
|
-
stop():
|
|
155
|
-
if
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (!this.process.killed) {
|
|
160
|
-
this.process.kill();
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
this.process = undefined;
|
|
75
|
+
stop():void{
|
|
76
|
+
if(this.process && !this.process.killed) this.process.kill();
|
|
164
77
|
}
|
|
165
|
-
}
|
|
78
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,15 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"outDir": "dist",
|
|
7
|
-
"rootDir": "src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true
|
|
11
|
-
},
|
|
12
|
-
"include": [
|
|
13
|
-
"src"
|
|
14
|
-
]
|
|
15
|
-
}
|
|
1
|
+
{"compilerOptions":{"target":"ES2022","module":"NodeNext","moduleResolution":"NodeNext","outDir":"dist","rootDir":"src","strict":true,"esModuleInterop":true,"skipLibCheck":true},"include":["src/**/*.ts"]}
|
package/.npm-global/crayon
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
-
|
|
4
|
-
case `uname` in
|
|
5
|
-
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
-
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
-
basedir=`cygpath -w "$basedir"`
|
|
8
|
-
fi
|
|
9
|
-
;;
|
|
10
|
-
esac
|
|
11
|
-
|
|
12
|
-
if [ -x "$basedir/node" ]; then
|
|
13
|
-
exec "$basedir/node" "$basedir/node_modules/Crayon-cli-local-gemma/dist/index.js" "$@"
|
|
14
|
-
else
|
|
15
|
-
exec node "$basedir/node_modules/Crayon-cli-local-gemma/dist/index.js" "$@"
|
|
16
|
-
fi
|
package/.npm-global/crayon.cmd
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
@ECHO off
|
|
2
|
-
GOTO start
|
|
3
|
-
:find_dp0
|
|
4
|
-
SET dp0=%~dp0
|
|
5
|
-
EXIT /b
|
|
6
|
-
:start
|
|
7
|
-
SETLOCAL
|
|
8
|
-
CALL :find_dp0
|
|
9
|
-
|
|
10
|
-
IF EXIST "%dp0%\node.exe" (
|
|
11
|
-
SET "_prog=%dp0%\node.exe"
|
|
12
|
-
) ELSE (
|
|
13
|
-
SET "_prog=node"
|
|
14
|
-
SET PATHEXT=%PATHEXT:;.JS;=;%
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\node_modules\Crayon-cli-local-gemma\dist\index.js" %*
|
package/.npm-global/crayon.ps1
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env pwsh
|
|
2
|
-
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
|
3
|
-
|
|
4
|
-
$exe=""
|
|
5
|
-
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
|
6
|
-
# Fix case when both the Windows and Linux builds of Node
|
|
7
|
-
# are installed in the same directory
|
|
8
|
-
$exe=".exe"
|
|
9
|
-
}
|
|
10
|
-
$ret=0
|
|
11
|
-
if (Test-Path "$basedir/node$exe") {
|
|
12
|
-
# Support pipeline input
|
|
13
|
-
if ($MyInvocation.ExpectingInput) {
|
|
14
|
-
$input | & "$basedir/node$exe" "$basedir/node_modules/Crayon-cli-local-gemma/dist/index.js" $args
|
|
15
|
-
} else {
|
|
16
|
-
& "$basedir/node$exe" "$basedir/node_modules/Crayon-cli-local-gemma/dist/index.js" $args
|
|
17
|
-
}
|
|
18
|
-
$ret=$LASTEXITCODE
|
|
19
|
-
} else {
|
|
20
|
-
# Support pipeline input
|
|
21
|
-
if ($MyInvocation.ExpectingInput) {
|
|
22
|
-
$input | & "node$exe" "$basedir/node_modules/Crayon-cli-local-gemma/dist/index.js" $args
|
|
23
|
-
} else {
|
|
24
|
-
& "node$exe" "$basedir/node_modules/Crayon-cli-local-gemma/dist/index.js" $args
|
|
25
|
-
}
|
|
26
|
-
$ret=$LASTEXITCODE
|
|
27
|
-
}
|
|
28
|
-
exit $ret
|