@ljoukov/llm 0.1.2 → 2.0.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/README.md +214 -12
- package/dist/index.cjs +519 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -6
- package/dist/index.d.ts +53 -6
- package/dist/index.js +518 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,8 +25,10 @@ npm i @ljoukov/llm
|
|
|
25
25
|
|
|
26
26
|
## Environment variables
|
|
27
27
|
|
|
28
|
-
This package
|
|
29
|
-
plain environment variables.
|
|
28
|
+
This package optionally loads a `.env.local` file from `process.cwd()` (Node.js) on first use (dotenv-style `KEY=value`
|
|
29
|
+
syntax) and does not override already-set `process.env` values. It always falls back to plain environment variables.
|
|
30
|
+
|
|
31
|
+
See Node.js docs on environment variables and dotenv files: https://nodejs.org/api/environment_variables.html#dotenv
|
|
30
32
|
|
|
31
33
|
### OpenAI
|
|
32
34
|
|
|
@@ -36,20 +38,52 @@ plain environment variables.
|
|
|
36
38
|
|
|
37
39
|
- `GOOGLE_SERVICE_ACCOUNT_JSON` (the contents of a service account JSON key file, not a file path)
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
#### Get a service account key JSON
|
|
42
|
+
|
|
43
|
+
You need a **Google service account key JSON** for your Firebase / GCP project (this is what you put into
|
|
44
|
+
`GOOGLE_SERVICE_ACCOUNT_JSON`).
|
|
45
|
+
|
|
46
|
+
- **Firebase Console:** your project -> Project settings -> **Service accounts** -> **Generate new private key**
|
|
47
|
+
- **Google Cloud Console:** IAM & Admin -> **Service Accounts** -> select/create an account -> **Keys** -> **Add key** ->
|
|
48
|
+
**Create new key** -> JSON
|
|
49
|
+
|
|
50
|
+
Either path is enough. Both produce the same kind of service account key `.json` file.
|
|
51
|
+
|
|
52
|
+
Official docs: https://docs.cloud.google.com/iam/docs/keys-create-delete
|
|
53
|
+
|
|
54
|
+
Store the JSON on one line (recommended):
|
|
40
55
|
|
|
41
56
|
```bash
|
|
42
57
|
jq -c . < path/to/service-account.json
|
|
43
58
|
```
|
|
44
59
|
|
|
60
|
+
Set it for local dev:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
export GOOGLE_SERVICE_ACCOUNT_JSON="$(jq -c . < path/to/service-account.json)"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If deploying to Cloudflare Workers/Pages:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
jq -c . < path/to/service-account.json | wrangler secret put GOOGLE_SERVICE_ACCOUNT_JSON
|
|
70
|
+
```
|
|
71
|
+
|
|
45
72
|
### ChatGPT subscription models
|
|
46
73
|
|
|
47
74
|
- `CHATGPT_AUTH_JSON_B64`
|
|
48
75
|
|
|
49
|
-
This is a base64url-encoded JSON blob containing the ChatGPT OAuth tokens + account id (
|
|
76
|
+
This is a base64url-encoded JSON blob containing the ChatGPT OAuth tokens + account id (RFC 4648):
|
|
77
|
+
https://www.rfc-editor.org/rfc/rfc4648
|
|
50
78
|
|
|
51
79
|
## Usage
|
|
52
80
|
|
|
81
|
+
`v2` uses OpenAI-style request fields:
|
|
82
|
+
|
|
83
|
+
- `input`: string or message array
|
|
84
|
+
- `instructions`: optional top-level system instructions
|
|
85
|
+
- message roles: `developer`, `system`, `user`, `assistant`
|
|
86
|
+
|
|
53
87
|
### Basic (non-streaming)
|
|
54
88
|
|
|
55
89
|
```ts
|
|
@@ -57,7 +91,7 @@ import { generateText } from "@ljoukov/llm";
|
|
|
57
91
|
|
|
58
92
|
const result = await generateText({
|
|
59
93
|
model: "gpt-5.2",
|
|
60
|
-
|
|
94
|
+
input: "Write one sentence about TypeScript.",
|
|
61
95
|
});
|
|
62
96
|
|
|
63
97
|
console.log(result.text);
|
|
@@ -71,7 +105,7 @@ import { streamText } from "@ljoukov/llm";
|
|
|
71
105
|
|
|
72
106
|
const call = streamText({
|
|
73
107
|
model: "gpt-5.2",
|
|
74
|
-
|
|
108
|
+
input: "Explain what a hash function is in one paragraph.",
|
|
75
109
|
});
|
|
76
110
|
|
|
77
111
|
for await (const event of call.events) {
|
|
@@ -90,6 +124,114 @@ const result = await call.result;
|
|
|
90
124
|
console.log("\nmodelVersion:", result.modelVersion);
|
|
91
125
|
```
|
|
92
126
|
|
|
127
|
+
### Full conversation (multi-turn)
|
|
128
|
+
|
|
129
|
+
Pass a full message array via `input`.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { generateText, type LlmInputMessage } from "@ljoukov/llm";
|
|
133
|
+
|
|
134
|
+
const input: LlmInputMessage[] = [
|
|
135
|
+
{
|
|
136
|
+
role: "system",
|
|
137
|
+
content: "You are a concise assistant.",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
role: "user",
|
|
141
|
+
content: "Summarize: Rust is a systems programming language.",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
role: "assistant",
|
|
145
|
+
content: "Rust is a fast, memory-safe systems language.",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
role: "user",
|
|
149
|
+
content: "Now rewrite it in 1 sentence.",
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const result = await generateText({ model: "gpt-5.2", input });
|
|
154
|
+
console.log(result.text);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Attachments (files / images)
|
|
158
|
+
|
|
159
|
+
Use `inlineData` parts to attach base64-encoded bytes (intermixed with text). `inlineData.data` is base64 (not a data
|
|
160
|
+
URL).
|
|
161
|
+
|
|
162
|
+
Note: `inlineData` is mapped based on `mimeType`.
|
|
163
|
+
|
|
164
|
+
- `image/*` -> image input (`input_image`)
|
|
165
|
+
- otherwise -> file input (`input_file`, e.g. `application/pdf`)
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import fs from "node:fs";
|
|
169
|
+
import { generateText, type LlmInputMessage } from "@ljoukov/llm";
|
|
170
|
+
|
|
171
|
+
const imageB64 = fs.readFileSync("image.png").toString("base64");
|
|
172
|
+
|
|
173
|
+
const input: LlmInputMessage[] = [
|
|
174
|
+
{
|
|
175
|
+
role: "user",
|
|
176
|
+
content: [
|
|
177
|
+
{ type: "text", text: "Describe this image in 1 paragraph." },
|
|
178
|
+
{ type: "inlineData", mimeType: "image/png", data: imageB64 },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const result = await generateText({ model: "gpt-5.2", input });
|
|
184
|
+
console.log(result.text);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
PDF attachment example:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import fs from "node:fs";
|
|
191
|
+
import { generateText, type LlmInputMessage } from "@ljoukov/llm";
|
|
192
|
+
|
|
193
|
+
const pdfB64 = fs.readFileSync("doc.pdf").toString("base64");
|
|
194
|
+
|
|
195
|
+
const input: LlmInputMessage[] = [
|
|
196
|
+
{
|
|
197
|
+
role: "user",
|
|
198
|
+
content: [
|
|
199
|
+
{ type: "text", text: "Summarize this PDF in 5 bullet points." },
|
|
200
|
+
{ type: "inlineData", mimeType: "application/pdf", data: pdfB64 },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
const result = await generateText({ model: "gpt-5.2", input });
|
|
206
|
+
console.log(result.text);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Intermixed text + multiple images (e.g. compare two images):
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import fs from "node:fs";
|
|
213
|
+
import { generateText, type LlmInputMessage } from "@ljoukov/llm";
|
|
214
|
+
|
|
215
|
+
const a = fs.readFileSync("a.png").toString("base64");
|
|
216
|
+
const b = fs.readFileSync("b.png").toString("base64");
|
|
217
|
+
|
|
218
|
+
const input: LlmInputMessage[] = [
|
|
219
|
+
{
|
|
220
|
+
role: "user",
|
|
221
|
+
content: [
|
|
222
|
+
{ type: "text", text: "Compare the two images. List the important differences." },
|
|
223
|
+
{ type: "text", text: "Image A:" },
|
|
224
|
+
{ type: "inlineData", mimeType: "image/png", data: a },
|
|
225
|
+
{ type: "text", text: "Image B:" },
|
|
226
|
+
{ type: "inlineData", mimeType: "image/png", data: b },
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const result = await generateText({ model: "gpt-5.2", input });
|
|
232
|
+
console.log(result.text);
|
|
233
|
+
```
|
|
234
|
+
|
|
93
235
|
### Gemini
|
|
94
236
|
|
|
95
237
|
```ts
|
|
@@ -97,7 +239,7 @@ import { generateText } from "@ljoukov/llm";
|
|
|
97
239
|
|
|
98
240
|
const result = await generateText({
|
|
99
241
|
model: "gemini-2.5-pro",
|
|
100
|
-
|
|
242
|
+
input: "Return exactly: OK",
|
|
101
243
|
});
|
|
102
244
|
|
|
103
245
|
console.log(result.text);
|
|
@@ -112,7 +254,7 @@ import { generateText } from "@ljoukov/llm";
|
|
|
112
254
|
|
|
113
255
|
const result = await generateText({
|
|
114
256
|
model: "chatgpt-gpt-5.1-codex-mini",
|
|
115
|
-
|
|
257
|
+
input: "Return exactly: OK",
|
|
116
258
|
});
|
|
117
259
|
|
|
118
260
|
console.log(result.text);
|
|
@@ -124,7 +266,8 @@ console.log(result.text);
|
|
|
124
266
|
|
|
125
267
|
- OpenAI API models use structured outputs (`json_schema`) when possible.
|
|
126
268
|
- Gemini uses `responseJsonSchema`.
|
|
127
|
-
- `chatgpt-*` models
|
|
269
|
+
- `chatgpt-*` models try to use structured outputs too; if rejected by the endpoint/model, it falls back to best-effort
|
|
270
|
+
JSON parsing.
|
|
128
271
|
|
|
129
272
|
```ts
|
|
130
273
|
import { generateJson } from "@ljoukov/llm";
|
|
@@ -137,13 +280,72 @@ const schema = z.object({
|
|
|
137
280
|
|
|
138
281
|
const { value } = await generateJson({
|
|
139
282
|
model: "gpt-5.2",
|
|
140
|
-
|
|
283
|
+
input: "Return a JSON object with ok=true and message='hello'.",
|
|
141
284
|
schema,
|
|
142
285
|
});
|
|
143
286
|
|
|
144
287
|
console.log(value.ok, value.message);
|
|
145
288
|
```
|
|
146
289
|
|
|
290
|
+
### Streaming JSON outputs
|
|
291
|
+
|
|
292
|
+
Use `streamJson()` to stream thought deltas and get best-effort partial JSON snapshots while the model is still
|
|
293
|
+
generating.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
import { streamJson } from "@ljoukov/llm";
|
|
297
|
+
import { z } from "zod";
|
|
298
|
+
|
|
299
|
+
const schema = z.object({
|
|
300
|
+
ok: z.boolean(),
|
|
301
|
+
message: z.string(),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const call = streamJson({
|
|
305
|
+
model: "gpt-5.2",
|
|
306
|
+
input: "Return a JSON object with ok=true and message='hello'.",
|
|
307
|
+
schema,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
for await (const event of call.events) {
|
|
311
|
+
if (event.type === "delta" && event.channel === "thought") {
|
|
312
|
+
process.stdout.write(event.text);
|
|
313
|
+
}
|
|
314
|
+
if (event.type === "json" && event.stage === "partial") {
|
|
315
|
+
console.log("partial:", event.value);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const { value } = await call.result;
|
|
320
|
+
console.log("final:", value);
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
If you only want thought deltas (no partial JSON), set `streamMode: "final"`.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
const call = streamJson({
|
|
327
|
+
model: "gpt-5.2",
|
|
328
|
+
input: "Return a JSON object with ok=true and message='hello'.",
|
|
329
|
+
schema,
|
|
330
|
+
streamMode: "final",
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
If you want to keep `generateJson()` but still stream thoughts, pass an `onEvent` callback.
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
const { value } = await generateJson({
|
|
338
|
+
model: "gpt-5.2",
|
|
339
|
+
input: "Return a JSON object with ok=true and message='hello'.",
|
|
340
|
+
schema,
|
|
341
|
+
onEvent: (event) => {
|
|
342
|
+
if (event.type === "delta" && event.channel === "thought") {
|
|
343
|
+
process.stdout.write(event.text);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
147
349
|
## Tools
|
|
148
350
|
|
|
149
351
|
This library supports two kinds of tools:
|
|
@@ -160,7 +362,7 @@ import { generateText } from "@ljoukov/llm";
|
|
|
160
362
|
|
|
161
363
|
const result = await generateText({
|
|
162
364
|
model: "gpt-5.2",
|
|
163
|
-
|
|
365
|
+
input: "Find 3 relevant sources about X and summarize them.",
|
|
164
366
|
tools: [{ type: "web-search", mode: "live" }, { type: "code-execution" }],
|
|
165
367
|
});
|
|
166
368
|
|
|
@@ -177,7 +379,7 @@ import { z } from "zod";
|
|
|
177
379
|
|
|
178
380
|
const result = await runToolLoop({
|
|
179
381
|
model: "gpt-5.2",
|
|
180
|
-
|
|
382
|
+
input: "What is 12 * 9? Use the tool.",
|
|
181
383
|
tools: {
|
|
182
384
|
multiply: tool({
|
|
183
385
|
description: "Multiply two integers.",
|