@ljoukov/llm 0.1.3 → 2.1.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 +175 -31
- package/dist/index.cjs +2167 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +254 -6
- package/dist/index.d.ts +254 -6
- package/dist/index.js +2154 -41
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -78,6 +78,12 @@ https://www.rfc-editor.org/rfc/rfc4648
|
|
|
78
78
|
|
|
79
79
|
## Usage
|
|
80
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
|
+
|
|
81
87
|
### Basic (non-streaming)
|
|
82
88
|
|
|
83
89
|
```ts
|
|
@@ -85,7 +91,7 @@ import { generateText } from "@ljoukov/llm";
|
|
|
85
91
|
|
|
86
92
|
const result = await generateText({
|
|
87
93
|
model: "gpt-5.2",
|
|
88
|
-
|
|
94
|
+
input: "Write one sentence about TypeScript.",
|
|
89
95
|
});
|
|
90
96
|
|
|
91
97
|
console.log(result.text);
|
|
@@ -99,7 +105,7 @@ import { streamText } from "@ljoukov/llm";
|
|
|
99
105
|
|
|
100
106
|
const call = streamText({
|
|
101
107
|
model: "gpt-5.2",
|
|
102
|
-
|
|
108
|
+
input: "Explain what a hash function is in one paragraph.",
|
|
103
109
|
});
|
|
104
110
|
|
|
105
111
|
for await (const event of call.events) {
|
|
@@ -120,33 +126,31 @@ console.log("\nmodelVersion:", result.modelVersion);
|
|
|
120
126
|
|
|
121
127
|
### Full conversation (multi-turn)
|
|
122
128
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
Note: assistant messages use `role: "model"`.
|
|
129
|
+
Pass a full message array via `input`.
|
|
126
130
|
|
|
127
131
|
```ts
|
|
128
|
-
import { generateText, type
|
|
132
|
+
import { generateText, type LlmInputMessage } from "@ljoukov/llm";
|
|
129
133
|
|
|
130
|
-
const
|
|
134
|
+
const input: LlmInputMessage[] = [
|
|
131
135
|
{
|
|
132
136
|
role: "system",
|
|
133
|
-
|
|
137
|
+
content: "You are a concise assistant.",
|
|
134
138
|
},
|
|
135
139
|
{
|
|
136
140
|
role: "user",
|
|
137
|
-
|
|
141
|
+
content: "Summarize: Rust is a systems programming language.",
|
|
138
142
|
},
|
|
139
143
|
{
|
|
140
|
-
role: "
|
|
141
|
-
|
|
144
|
+
role: "assistant",
|
|
145
|
+
content: "Rust is a fast, memory-safe systems language.",
|
|
142
146
|
},
|
|
143
147
|
{
|
|
144
148
|
role: "user",
|
|
145
|
-
|
|
149
|
+
content: "Now rewrite it in 1 sentence.",
|
|
146
150
|
},
|
|
147
151
|
];
|
|
148
152
|
|
|
149
|
-
const result = await generateText({ model: "gpt-5.2",
|
|
153
|
+
const result = await generateText({ model: "gpt-5.2", input });
|
|
150
154
|
console.log(result.text);
|
|
151
155
|
```
|
|
152
156
|
|
|
@@ -162,21 +166,21 @@ Note: `inlineData` is mapped based on `mimeType`.
|
|
|
162
166
|
|
|
163
167
|
```ts
|
|
164
168
|
import fs from "node:fs";
|
|
165
|
-
import { generateText, type
|
|
169
|
+
import { generateText, type LlmInputMessage } from "@ljoukov/llm";
|
|
166
170
|
|
|
167
171
|
const imageB64 = fs.readFileSync("image.png").toString("base64");
|
|
168
172
|
|
|
169
|
-
const
|
|
173
|
+
const input: LlmInputMessage[] = [
|
|
170
174
|
{
|
|
171
175
|
role: "user",
|
|
172
|
-
|
|
176
|
+
content: [
|
|
173
177
|
{ type: "text", text: "Describe this image in 1 paragraph." },
|
|
174
178
|
{ type: "inlineData", mimeType: "image/png", data: imageB64 },
|
|
175
179
|
],
|
|
176
180
|
},
|
|
177
181
|
];
|
|
178
182
|
|
|
179
|
-
const result = await generateText({ model: "gpt-5.2",
|
|
183
|
+
const result = await generateText({ model: "gpt-5.2", input });
|
|
180
184
|
console.log(result.text);
|
|
181
185
|
```
|
|
182
186
|
|
|
@@ -184,21 +188,21 @@ PDF attachment example:
|
|
|
184
188
|
|
|
185
189
|
```ts
|
|
186
190
|
import fs from "node:fs";
|
|
187
|
-
import { generateText, type
|
|
191
|
+
import { generateText, type LlmInputMessage } from "@ljoukov/llm";
|
|
188
192
|
|
|
189
193
|
const pdfB64 = fs.readFileSync("doc.pdf").toString("base64");
|
|
190
194
|
|
|
191
|
-
const
|
|
195
|
+
const input: LlmInputMessage[] = [
|
|
192
196
|
{
|
|
193
197
|
role: "user",
|
|
194
|
-
|
|
198
|
+
content: [
|
|
195
199
|
{ type: "text", text: "Summarize this PDF in 5 bullet points." },
|
|
196
200
|
{ type: "inlineData", mimeType: "application/pdf", data: pdfB64 },
|
|
197
201
|
],
|
|
198
202
|
},
|
|
199
203
|
];
|
|
200
204
|
|
|
201
|
-
const result = await generateText({ model: "gpt-5.2",
|
|
205
|
+
const result = await generateText({ model: "gpt-5.2", input });
|
|
202
206
|
console.log(result.text);
|
|
203
207
|
```
|
|
204
208
|
|
|
@@ -206,15 +210,15 @@ Intermixed text + multiple images (e.g. compare two images):
|
|
|
206
210
|
|
|
207
211
|
```ts
|
|
208
212
|
import fs from "node:fs";
|
|
209
|
-
import { generateText, type
|
|
213
|
+
import { generateText, type LlmInputMessage } from "@ljoukov/llm";
|
|
210
214
|
|
|
211
215
|
const a = fs.readFileSync("a.png").toString("base64");
|
|
212
216
|
const b = fs.readFileSync("b.png").toString("base64");
|
|
213
217
|
|
|
214
|
-
const
|
|
218
|
+
const input: LlmInputMessage[] = [
|
|
215
219
|
{
|
|
216
220
|
role: "user",
|
|
217
|
-
|
|
221
|
+
content: [
|
|
218
222
|
{ type: "text", text: "Compare the two images. List the important differences." },
|
|
219
223
|
{ type: "text", text: "Image A:" },
|
|
220
224
|
{ type: "inlineData", mimeType: "image/png", data: a },
|
|
@@ -224,7 +228,7 @@ const contents: LlmContent[] = [
|
|
|
224
228
|
},
|
|
225
229
|
];
|
|
226
230
|
|
|
227
|
-
const result = await generateText({ model: "gpt-5.2",
|
|
231
|
+
const result = await generateText({ model: "gpt-5.2", input });
|
|
228
232
|
console.log(result.text);
|
|
229
233
|
```
|
|
230
234
|
|
|
@@ -235,7 +239,7 @@ import { generateText } from "@ljoukov/llm";
|
|
|
235
239
|
|
|
236
240
|
const result = await generateText({
|
|
237
241
|
model: "gemini-2.5-pro",
|
|
238
|
-
|
|
242
|
+
input: "Return exactly: OK",
|
|
239
243
|
});
|
|
240
244
|
|
|
241
245
|
console.log(result.text);
|
|
@@ -250,7 +254,7 @@ import { generateText } from "@ljoukov/llm";
|
|
|
250
254
|
|
|
251
255
|
const result = await generateText({
|
|
252
256
|
model: "chatgpt-gpt-5.1-codex-mini",
|
|
253
|
-
|
|
257
|
+
input: "Return exactly: OK",
|
|
254
258
|
});
|
|
255
259
|
|
|
256
260
|
console.log(result.text);
|
|
@@ -262,7 +266,8 @@ console.log(result.text);
|
|
|
262
266
|
|
|
263
267
|
- OpenAI API models use structured outputs (`json_schema`) when possible.
|
|
264
268
|
- Gemini uses `responseJsonSchema`.
|
|
265
|
-
- `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.
|
|
266
271
|
|
|
267
272
|
```ts
|
|
268
273
|
import { generateJson } from "@ljoukov/llm";
|
|
@@ -275,13 +280,72 @@ const schema = z.object({
|
|
|
275
280
|
|
|
276
281
|
const { value } = await generateJson({
|
|
277
282
|
model: "gpt-5.2",
|
|
278
|
-
|
|
283
|
+
input: "Return a JSON object with ok=true and message='hello'.",
|
|
279
284
|
schema,
|
|
280
285
|
});
|
|
281
286
|
|
|
282
287
|
console.log(value.ok, value.message);
|
|
283
288
|
```
|
|
284
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
|
+
|
|
285
349
|
## Tools
|
|
286
350
|
|
|
287
351
|
This library supports two kinds of tools:
|
|
@@ -298,7 +362,7 @@ import { generateText } from "@ljoukov/llm";
|
|
|
298
362
|
|
|
299
363
|
const result = await generateText({
|
|
300
364
|
model: "gpt-5.2",
|
|
301
|
-
|
|
365
|
+
input: "Find 3 relevant sources about X and summarize them.",
|
|
302
366
|
tools: [{ type: "web-search", mode: "live" }, { type: "code-execution" }],
|
|
303
367
|
});
|
|
304
368
|
|
|
@@ -315,7 +379,7 @@ import { z } from "zod";
|
|
|
315
379
|
|
|
316
380
|
const result = await runToolLoop({
|
|
317
381
|
model: "gpt-5.2",
|
|
318
|
-
|
|
382
|
+
input: "What is 12 * 9? Use the tool.",
|
|
319
383
|
tools: {
|
|
320
384
|
multiply: tool({
|
|
321
385
|
description: "Multiply two integers.",
|
|
@@ -328,6 +392,86 @@ const result = await runToolLoop({
|
|
|
328
392
|
console.log(result.text);
|
|
329
393
|
```
|
|
330
394
|
|
|
395
|
+
### Built-in `apply_patch` tool
|
|
396
|
+
|
|
397
|
+
The library includes a Codex-style `apply_patch` tool with a pluggable filesystem adapter.
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
import {
|
|
401
|
+
createApplyPatchTool,
|
|
402
|
+
createInMemoryAgentFilesystem,
|
|
403
|
+
runToolLoop,
|
|
404
|
+
} from "@ljoukov/llm";
|
|
405
|
+
|
|
406
|
+
const fs = createInMemoryAgentFilesystem({
|
|
407
|
+
"/repo/index.ts": "export const value = 1;\n",
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const result = await runToolLoop({
|
|
411
|
+
model: "chatgpt-gpt-5.3-codex",
|
|
412
|
+
input: "Use apply_patch to change value from 1 to 2.",
|
|
413
|
+
tools: {
|
|
414
|
+
apply_patch: createApplyPatchTool({
|
|
415
|
+
cwd: "/repo",
|
|
416
|
+
fs,
|
|
417
|
+
checkAccess: ({ path }) => {
|
|
418
|
+
if (!path.startsWith("/repo/")) {
|
|
419
|
+
throw new Error("Writes are allowed only inside /repo");
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
}),
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
console.log(result.text);
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### `runAgentLoop()` with model-aware filesystem tools
|
|
430
|
+
|
|
431
|
+
Use `runAgentLoop()` when you want a default filesystem toolset chosen by model:
|
|
432
|
+
|
|
433
|
+
- Codex-like models -> `apply_patch`, `read_file`, `list_dir`, `grep_files`
|
|
434
|
+
- Gemini models -> `read_file`, `write_file`, `replace`, `list_directory`, `grep_search`, `glob`
|
|
435
|
+
- Other models -> model-agnostic (Gemini-style) set by default
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
import { createInMemoryAgentFilesystem, runAgentLoop } from "@ljoukov/llm";
|
|
439
|
+
|
|
440
|
+
const fs = createInMemoryAgentFilesystem({
|
|
441
|
+
"/repo/src/a.ts": "export const value = 1;\n",
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const result = await runAgentLoop({
|
|
445
|
+
model: "chatgpt-gpt-5.3-codex",
|
|
446
|
+
input: "Change value from 1 to 2 using filesystem tools.",
|
|
447
|
+
filesystemTool: {
|
|
448
|
+
profile: "auto",
|
|
449
|
+
options: {
|
|
450
|
+
cwd: "/repo",
|
|
451
|
+
fs,
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
console.log(result.text);
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## Agent benchmark (micro)
|
|
460
|
+
|
|
461
|
+
For small edit-harness experiments with `chatgpt-gpt-5.3-codex`:
|
|
462
|
+
|
|
463
|
+
```bash
|
|
464
|
+
npm run bench:agent
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Estimate-only:
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
npm run bench:agent:estimate
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
See `benchmarks/agent/README.md` for options and output format.
|
|
474
|
+
|
|
331
475
|
## License
|
|
332
476
|
|
|
333
477
|
MIT
|