@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 CHANGED
@@ -25,8 +25,10 @@ npm i @ljoukov/llm
25
25
 
26
26
  ## Environment variables
27
27
 
28
- This package reads a `.env.local` file in `process.cwd()` (Node.js) using the same rules as Spark, and falls back to
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
- For local dev it is usually easiest to store the JSON on one line:
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 (Spark-compatible).
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
- prompt: "Write one sentence about TypeScript.",
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
- prompt: "Explain what a hash function is in one paragraph.",
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
- prompt: "Return exactly: OK",
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
- prompt: "Return exactly: OK",
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 fall back to best-effort JSON parsing (no strict schema mode).
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
- prompt: "Return a JSON object with ok=true and message='hello'.",
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
- prompt: "Find 3 relevant sources about X and summarize them.",
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
- prompt: "What is 12 * 9? Use the tool.",
382
+ input: "What is 12 * 9? Use the tool.",
181
383
  tools: {
182
384
  multiply: tool({
183
385
  description: "Multiply two integers.",