@ljoukov/llm 0.1.0 → 0.1.3
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 +142 -4
- package/dist/index.cjs +44 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +44 -11
- package/dist/index.js.map +1 -1
- package/package.json +20 -2
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,17 +38,43 @@ 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
|
|
|
@@ -90,6 +118,116 @@ const result = await call.result;
|
|
|
90
118
|
console.log("\nmodelVersion:", result.modelVersion);
|
|
91
119
|
```
|
|
92
120
|
|
|
121
|
+
### Full conversation (multi-turn)
|
|
122
|
+
|
|
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"`.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { generateText, type LlmContent } from "@ljoukov/llm";
|
|
129
|
+
|
|
130
|
+
const contents: LlmContent[] = [
|
|
131
|
+
{
|
|
132
|
+
role: "system",
|
|
133
|
+
parts: [{ type: "text", text: "You are a concise assistant." }],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
role: "user",
|
|
137
|
+
parts: [{ type: "text", text: "Summarize: Rust is a systems programming language." }],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
role: "model",
|
|
141
|
+
parts: [{ type: "text", text: "Rust is a fast, memory-safe systems language." }],
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
role: "user",
|
|
145
|
+
parts: [{ type: "text", text: "Now rewrite it in 1 sentence." }],
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const result = await generateText({ model: "gpt-5.2", contents });
|
|
150
|
+
console.log(result.text);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Attachments (files / images)
|
|
154
|
+
|
|
155
|
+
Use `inlineData` parts to attach base64-encoded bytes (intermixed with text). `inlineData.data` is base64 (not a data
|
|
156
|
+
URL).
|
|
157
|
+
|
|
158
|
+
Note: `inlineData` is mapped based on `mimeType`.
|
|
159
|
+
|
|
160
|
+
- `image/*` -> image input (`input_image`)
|
|
161
|
+
- otherwise -> file input (`input_file`, e.g. `application/pdf`)
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import fs from "node:fs";
|
|
165
|
+
import { generateText, type LlmContent } from "@ljoukov/llm";
|
|
166
|
+
|
|
167
|
+
const imageB64 = fs.readFileSync("image.png").toString("base64");
|
|
168
|
+
|
|
169
|
+
const contents: LlmContent[] = [
|
|
170
|
+
{
|
|
171
|
+
role: "user",
|
|
172
|
+
parts: [
|
|
173
|
+
{ type: "text", text: "Describe this image in 1 paragraph." },
|
|
174
|
+
{ type: "inlineData", mimeType: "image/png", data: imageB64 },
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const result = await generateText({ model: "gpt-5.2", contents });
|
|
180
|
+
console.log(result.text);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
PDF attachment example:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import fs from "node:fs";
|
|
187
|
+
import { generateText, type LlmContent } from "@ljoukov/llm";
|
|
188
|
+
|
|
189
|
+
const pdfB64 = fs.readFileSync("doc.pdf").toString("base64");
|
|
190
|
+
|
|
191
|
+
const contents: LlmContent[] = [
|
|
192
|
+
{
|
|
193
|
+
role: "user",
|
|
194
|
+
parts: [
|
|
195
|
+
{ type: "text", text: "Summarize this PDF in 5 bullet points." },
|
|
196
|
+
{ type: "inlineData", mimeType: "application/pdf", data: pdfB64 },
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const result = await generateText({ model: "gpt-5.2", contents });
|
|
202
|
+
console.log(result.text);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Intermixed text + multiple images (e.g. compare two images):
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import fs from "node:fs";
|
|
209
|
+
import { generateText, type LlmContent } from "@ljoukov/llm";
|
|
210
|
+
|
|
211
|
+
const a = fs.readFileSync("a.png").toString("base64");
|
|
212
|
+
const b = fs.readFileSync("b.png").toString("base64");
|
|
213
|
+
|
|
214
|
+
const contents: LlmContent[] = [
|
|
215
|
+
{
|
|
216
|
+
role: "user",
|
|
217
|
+
parts: [
|
|
218
|
+
{ type: "text", text: "Compare the two images. List the important differences." },
|
|
219
|
+
{ type: "text", text: "Image A:" },
|
|
220
|
+
{ type: "inlineData", mimeType: "image/png", data: a },
|
|
221
|
+
{ type: "text", text: "Image B:" },
|
|
222
|
+
{ type: "inlineData", mimeType: "image/png", data: b },
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
const result = await generateText({ model: "gpt-5.2", contents });
|
|
228
|
+
console.log(result.text);
|
|
229
|
+
```
|
|
230
|
+
|
|
93
231
|
### Gemini
|
|
94
232
|
|
|
95
233
|
```ts
|
package/dist/index.cjs
CHANGED
|
@@ -1655,6 +1655,20 @@ function isInlineImageMime(mimeType) {
|
|
|
1655
1655
|
}
|
|
1656
1656
|
return mimeType.startsWith("image/");
|
|
1657
1657
|
}
|
|
1658
|
+
function guessInlineDataFilename(mimeType) {
|
|
1659
|
+
switch (mimeType) {
|
|
1660
|
+
case "application/pdf":
|
|
1661
|
+
return "document.pdf";
|
|
1662
|
+
case "application/json":
|
|
1663
|
+
return "data.json";
|
|
1664
|
+
case "text/markdown":
|
|
1665
|
+
return "document.md";
|
|
1666
|
+
case "text/plain":
|
|
1667
|
+
return "document.txt";
|
|
1668
|
+
default:
|
|
1669
|
+
return "attachment.bin";
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1658
1672
|
function mergeConsecutiveTextParts(parts) {
|
|
1659
1673
|
if (parts.length === 0) {
|
|
1660
1674
|
return [];
|
|
@@ -1797,9 +1811,18 @@ function toOpenAiInput(contents) {
|
|
|
1797
1811
|
parts.push({ type: "input_text", text: part.text });
|
|
1798
1812
|
continue;
|
|
1799
1813
|
}
|
|
1800
|
-
const mimeType = part.mimeType
|
|
1801
|
-
|
|
1802
|
-
|
|
1814
|
+
const mimeType = part.mimeType;
|
|
1815
|
+
if (isInlineImageMime(mimeType)) {
|
|
1816
|
+
const dataUrl = `data:${mimeType};base64,${part.data}`;
|
|
1817
|
+
parts.push({ type: "input_image", image_url: dataUrl, detail: "auto" });
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
const fileData = decodeInlineDataBuffer(part.data).toString("base64");
|
|
1821
|
+
parts.push({
|
|
1822
|
+
type: "input_file",
|
|
1823
|
+
filename: guessInlineDataFilename(mimeType),
|
|
1824
|
+
file_data: fileData
|
|
1825
|
+
});
|
|
1803
1826
|
}
|
|
1804
1827
|
if (parts.length === 1 && parts[0]?.type === "input_text" && typeof parts[0].text === "string") {
|
|
1805
1828
|
return {
|
|
@@ -1835,19 +1858,29 @@ function toChatGptInput(contents) {
|
|
|
1835
1858
|
});
|
|
1836
1859
|
continue;
|
|
1837
1860
|
}
|
|
1838
|
-
const mimeType = part.mimeType ?? "application/octet-stream";
|
|
1839
|
-
const dataUrl = `data:${mimeType};base64,${part.data}`;
|
|
1840
1861
|
if (isAssistant) {
|
|
1862
|
+
const mimeType = part.mimeType ?? "application/octet-stream";
|
|
1841
1863
|
parts.push({
|
|
1842
1864
|
type: "output_text",
|
|
1843
|
-
text: `[image:${mimeType}]`
|
|
1865
|
+
text: isInlineImageMime(part.mimeType) ? `[image:${mimeType}]` : `[file:${mimeType}]`
|
|
1844
1866
|
});
|
|
1845
1867
|
} else {
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1868
|
+
if (isInlineImageMime(part.mimeType)) {
|
|
1869
|
+
const mimeType = part.mimeType ?? "application/octet-stream";
|
|
1870
|
+
const dataUrl = `data:${mimeType};base64,${part.data}`;
|
|
1871
|
+
parts.push({
|
|
1872
|
+
type: "input_image",
|
|
1873
|
+
image_url: dataUrl,
|
|
1874
|
+
detail: "auto"
|
|
1875
|
+
});
|
|
1876
|
+
} else {
|
|
1877
|
+
const fileData = decodeInlineDataBuffer(part.data).toString("base64");
|
|
1878
|
+
parts.push({
|
|
1879
|
+
type: "input_file",
|
|
1880
|
+
filename: guessInlineDataFilename(part.mimeType),
|
|
1881
|
+
file_data: fileData
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1851
1884
|
}
|
|
1852
1885
|
}
|
|
1853
1886
|
if (parts.length === 0) {
|