@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 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
- prompt: "Write one sentence about TypeScript.",
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
- prompt: "Explain what a hash function is in one paragraph.",
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
- If you want to pass the full conversation (multiple user/assistant turns), use `contents` instead of `prompt`.
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 LlmContent } from "@ljoukov/llm";
132
+ import { generateText, type LlmInputMessage } from "@ljoukov/llm";
129
133
 
130
- const contents: LlmContent[] = [
134
+ const input: LlmInputMessage[] = [
131
135
  {
132
136
  role: "system",
133
- parts: [{ type: "text", text: "You are a concise assistant." }],
137
+ content: "You are a concise assistant.",
134
138
  },
135
139
  {
136
140
  role: "user",
137
- parts: [{ type: "text", text: "Summarize: Rust is a systems programming language." }],
141
+ content: "Summarize: Rust is a systems programming language.",
138
142
  },
139
143
  {
140
- role: "model",
141
- parts: [{ type: "text", text: "Rust is a fast, memory-safe systems language." }],
144
+ role: "assistant",
145
+ content: "Rust is a fast, memory-safe systems language.",
142
146
  },
143
147
  {
144
148
  role: "user",
145
- parts: [{ type: "text", text: "Now rewrite it in 1 sentence." }],
149
+ content: "Now rewrite it in 1 sentence.",
146
150
  },
147
151
  ];
148
152
 
149
- const result = await generateText({ model: "gpt-5.2", contents });
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 LlmContent } from "@ljoukov/llm";
169
+ import { generateText, type LlmInputMessage } from "@ljoukov/llm";
166
170
 
167
171
  const imageB64 = fs.readFileSync("image.png").toString("base64");
168
172
 
169
- const contents: LlmContent[] = [
173
+ const input: LlmInputMessage[] = [
170
174
  {
171
175
  role: "user",
172
- parts: [
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", contents });
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 LlmContent } from "@ljoukov/llm";
191
+ import { generateText, type LlmInputMessage } from "@ljoukov/llm";
188
192
 
189
193
  const pdfB64 = fs.readFileSync("doc.pdf").toString("base64");
190
194
 
191
- const contents: LlmContent[] = [
195
+ const input: LlmInputMessage[] = [
192
196
  {
193
197
  role: "user",
194
- parts: [
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", contents });
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 LlmContent } from "@ljoukov/llm";
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 contents: LlmContent[] = [
218
+ const input: LlmInputMessage[] = [
215
219
  {
216
220
  role: "user",
217
- parts: [
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", contents });
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
- prompt: "Return exactly: OK",
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
- prompt: "Return exactly: OK",
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 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.
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
- prompt: "Return a JSON object with ok=true and message='hello'.",
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
- prompt: "Find 3 relevant sources about X and summarize them.",
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
- prompt: "What is 12 * 9? Use the tool.",
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