@painitehq/structured-llm 0.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 +75 -0
- package/dist/index.cjs +332 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +62 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +323 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# React + TypeScript + Vite
|
|
2
|
+
|
|
3
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
4
|
+
|
|
5
|
+
Currently, two official plugins are available:
|
|
6
|
+
|
|
7
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
|
8
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
|
9
|
+
|
|
10
|
+
## React Compiler
|
|
11
|
+
|
|
12
|
+
The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information.
|
|
13
|
+
|
|
14
|
+
Note: This will impact Vite dev & build performances.
|
|
15
|
+
|
|
16
|
+
## Expanding the ESLint configuration
|
|
17
|
+
|
|
18
|
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
export default defineConfig([
|
|
22
|
+
globalIgnores(['dist']),
|
|
23
|
+
{
|
|
24
|
+
files: ['**/*.{ts,tsx}'],
|
|
25
|
+
extends: [
|
|
26
|
+
// Other configs...
|
|
27
|
+
|
|
28
|
+
// Remove tseslint.configs.recommended and replace with this
|
|
29
|
+
tseslint.configs.recommendedTypeChecked,
|
|
30
|
+
// Alternatively, use this for stricter rules
|
|
31
|
+
tseslint.configs.strictTypeChecked,
|
|
32
|
+
// Optionally, add this for stylistic rules
|
|
33
|
+
tseslint.configs.stylisticTypeChecked,
|
|
34
|
+
|
|
35
|
+
// Other configs...
|
|
36
|
+
],
|
|
37
|
+
languageOptions: {
|
|
38
|
+
parserOptions: {
|
|
39
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
40
|
+
tsconfigRootDir: import.meta.dirname,
|
|
41
|
+
},
|
|
42
|
+
// other options...
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
// eslint.config.js
|
|
52
|
+
import reactX from 'eslint-plugin-react-x'
|
|
53
|
+
import reactDom from 'eslint-plugin-react-dom'
|
|
54
|
+
|
|
55
|
+
export default defineConfig([
|
|
56
|
+
globalIgnores(['dist']),
|
|
57
|
+
{
|
|
58
|
+
files: ['**/*.{ts,tsx}'],
|
|
59
|
+
extends: [
|
|
60
|
+
// Other configs...
|
|
61
|
+
// Enable lint rules for React
|
|
62
|
+
reactX.configs['recommended-typescript'],
|
|
63
|
+
// Enable lint rules for React DOM
|
|
64
|
+
reactDom.configs.recommended,
|
|
65
|
+
],
|
|
66
|
+
languageOptions: {
|
|
67
|
+
parserOptions: {
|
|
68
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
69
|
+
tsconfigRootDir: import.meta.dirname,
|
|
70
|
+
},
|
|
71
|
+
// other options...
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
])
|
|
75
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/llm-client.ts
|
|
4
|
+
var DEFAULT_MODEL = "openrouter/free";
|
|
5
|
+
var DEFAULT_TEMPERATURE = 0;
|
|
6
|
+
var DEFAULT_TIMEOUT = 6e4;
|
|
7
|
+
async function callLLM(prompt, config) {
|
|
8
|
+
const {
|
|
9
|
+
apiKey,
|
|
10
|
+
model = DEFAULT_MODEL,
|
|
11
|
+
temperature = DEFAULT_TEMPERATURE,
|
|
12
|
+
timeout = DEFAULT_TIMEOUT
|
|
13
|
+
} = config;
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
Authorization: `Bearer ${apiKey}`,
|
|
22
|
+
"HTTP-Referer": "https://github.com/harshitduggal/structured-llm",
|
|
23
|
+
"X-Title": "structured-llm"
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
model,
|
|
27
|
+
messages: [
|
|
28
|
+
{
|
|
29
|
+
role: "system",
|
|
30
|
+
content: "You are a precise data extraction assistant. You MUST respond with valid JSON only. No explanations, no markdown, no text outside the JSON. Just the raw JSON object."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
role: "user",
|
|
34
|
+
content: prompt
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
temperature,
|
|
38
|
+
response_format: { type: "json_object" }
|
|
39
|
+
}),
|
|
40
|
+
signal: controller.signal
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const errorBody = await response.text();
|
|
44
|
+
throw new Error(
|
|
45
|
+
`OpenRouter API error: ${response.status} - ${errorBody}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const data = await response.json();
|
|
49
|
+
if (!data.choices || data.choices.length === 0) {
|
|
50
|
+
throw new Error("No response from LLM");
|
|
51
|
+
}
|
|
52
|
+
const content = data.choices[0].message?.content;
|
|
53
|
+
if (!content) {
|
|
54
|
+
throw new Error("Empty response from LLM");
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
content,
|
|
58
|
+
model: data.model || model,
|
|
59
|
+
usage: data.usage ? {
|
|
60
|
+
promptTokens: data.usage.prompt_tokens,
|
|
61
|
+
completionTokens: data.usage.completion_tokens,
|
|
62
|
+
totalTokens: data.usage.total_tokens
|
|
63
|
+
} : void 0
|
|
64
|
+
};
|
|
65
|
+
} finally {
|
|
66
|
+
clearTimeout(timeoutId);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/validator.ts
|
|
71
|
+
function tryParseJSON(raw) {
|
|
72
|
+
try {
|
|
73
|
+
const data = JSON.parse(raw);
|
|
74
|
+
return { success: true, data };
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return { success: false, error: e instanceof Error ? e.message : "Unknown parse error" };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function extractJSON(raw) {
|
|
80
|
+
const trimmed = raw.trim();
|
|
81
|
+
if (tryParseJSON(trimmed).success) {
|
|
82
|
+
return trimmed;
|
|
83
|
+
}
|
|
84
|
+
const codeBlockMatch = trimmed.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
85
|
+
if (codeBlockMatch) {
|
|
86
|
+
const inner = codeBlockMatch[1].trim();
|
|
87
|
+
if (tryParseJSON(inner).success) {
|
|
88
|
+
return inner;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const firstBrace = trimmed.indexOf("{");
|
|
92
|
+
const lastBrace = trimmed.lastIndexOf("}");
|
|
93
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
94
|
+
const candidate = trimmed.slice(firstBrace, lastBrace + 1);
|
|
95
|
+
if (tryParseJSON(candidate).success) {
|
|
96
|
+
return candidate;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const firstBracket = trimmed.indexOf("[");
|
|
100
|
+
const lastBracket = trimmed.lastIndexOf("]");
|
|
101
|
+
if (firstBracket !== -1 && lastBracket > firstBracket) {
|
|
102
|
+
const candidate = trimmed.slice(firstBracket, lastBracket + 1);
|
|
103
|
+
if (tryParseJSON(candidate).success) {
|
|
104
|
+
return candidate;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
function repairJSON(raw) {
|
|
110
|
+
let candidate = raw.trim();
|
|
111
|
+
candidate = candidate.replace(/,\s*([}\]])/g, "$1");
|
|
112
|
+
candidate = candidate.replace(/'/g, '"');
|
|
113
|
+
candidate = candidate.replace(/(\w+)\s*:/g, '"$1":');
|
|
114
|
+
candidate = candidate.replace(/:\s*"([^"]*?)"/g, (match, content) => {
|
|
115
|
+
if (content.includes('"')) {
|
|
116
|
+
return match;
|
|
117
|
+
}
|
|
118
|
+
return match;
|
|
119
|
+
});
|
|
120
|
+
if (!candidate.startsWith("{") && !candidate.startsWith("[")) {
|
|
121
|
+
const firstBrace = candidate.indexOf("{");
|
|
122
|
+
const firstBracket = candidate.indexOf("[");
|
|
123
|
+
if (firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket)) {
|
|
124
|
+
candidate = candidate.slice(firstBrace);
|
|
125
|
+
} else if (firstBracket !== -1) {
|
|
126
|
+
candidate = candidate.slice(firstBracket);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!candidate.endsWith("}") && !candidate.endsWith("]")) {
|
|
130
|
+
const lastBrace = candidate.lastIndexOf("}");
|
|
131
|
+
const lastBracket = candidate.lastIndexOf("]");
|
|
132
|
+
if (lastBrace !== -1 && (lastBracket === -1 || lastBrace > lastBracket)) {
|
|
133
|
+
candidate = candidate.slice(0, lastBrace + 1);
|
|
134
|
+
} else if (lastBracket !== -1) {
|
|
135
|
+
candidate = candidate.slice(0, lastBracket + 1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const result = tryParseJSON(candidate);
|
|
139
|
+
if (result.success) {
|
|
140
|
+
return candidate;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
function validateAgainstSchema(data, schema) {
|
|
145
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
146
|
+
return { valid: false, errors: ["Root value must be an object"] };
|
|
147
|
+
}
|
|
148
|
+
const errors = [];
|
|
149
|
+
const obj = data;
|
|
150
|
+
for (const [key, field] of Object.entries(schema)) {
|
|
151
|
+
const value = obj[key];
|
|
152
|
+
if (value === void 0 && field.required !== false) {
|
|
153
|
+
errors.push(`Missing required field: "${key}"`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (value === void 0) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
switch (field.type) {
|
|
160
|
+
case "string":
|
|
161
|
+
if (typeof value !== "string") {
|
|
162
|
+
errors.push(`Field "${key}" must be string, got ${typeof value}`);
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
case "number":
|
|
166
|
+
if (typeof value !== "number") {
|
|
167
|
+
errors.push(`Field "${key}" must be number, got ${typeof value}`);
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
case "boolean":
|
|
171
|
+
if (typeof value !== "boolean") {
|
|
172
|
+
errors.push(`Field "${key}" must be boolean, got ${typeof value}`);
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
case "array":
|
|
176
|
+
if (!Array.isArray(value)) {
|
|
177
|
+
errors.push(`Field "${key}" must be array, got ${typeof value}`);
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
case "object":
|
|
181
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
182
|
+
errors.push(`Field "${key}" must be object, got ${typeof value}`);
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (errors.length > 0) {
|
|
188
|
+
return { valid: false, errors };
|
|
189
|
+
}
|
|
190
|
+
return { valid: true, data };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/schema.ts
|
|
194
|
+
function defineSchema(name, fields) {
|
|
195
|
+
return { name, fields };
|
|
196
|
+
}
|
|
197
|
+
function schemaToPrompt(schema) {
|
|
198
|
+
const fieldDescriptions = Object.entries(schema.fields).map(([key, field]) => {
|
|
199
|
+
const parts = [`"${key}": ${fieldTypeToExample(field)}`];
|
|
200
|
+
if (field.description) {
|
|
201
|
+
parts.push(`// ${field.description}`);
|
|
202
|
+
}
|
|
203
|
+
return parts.join(" ");
|
|
204
|
+
}).join(",\n ");
|
|
205
|
+
return `{
|
|
206
|
+
"${schema.name}": {
|
|
207
|
+
${fieldDescriptions}
|
|
208
|
+
}
|
|
209
|
+
}`;
|
|
210
|
+
}
|
|
211
|
+
function fieldTypeToExample(field) {
|
|
212
|
+
switch (field.type) {
|
|
213
|
+
case "string":
|
|
214
|
+
return `"..."`;
|
|
215
|
+
case "number":
|
|
216
|
+
return "0";
|
|
217
|
+
case "boolean":
|
|
218
|
+
return "true";
|
|
219
|
+
case "array":
|
|
220
|
+
if (field.items) {
|
|
221
|
+
return `[${fieldTypeToExample(field.items)}]`;
|
|
222
|
+
}
|
|
223
|
+
return "[]";
|
|
224
|
+
case "object":
|
|
225
|
+
if (field.properties) {
|
|
226
|
+
const inner = Object.entries(field.properties).map(([k, v]) => `"${k}": ${fieldTypeToExample(v)}`).join(", ");
|
|
227
|
+
return `{ ${inner} }`;
|
|
228
|
+
}
|
|
229
|
+
return "{}";
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function schemaToZodishString(schema) {
|
|
233
|
+
const lines = [];
|
|
234
|
+
lines.push(`Expected JSON structure for "${schema.name}":`);
|
|
235
|
+
for (const [key, field] of Object.entries(schema.fields)) {
|
|
236
|
+
const required = field.required !== false ? "(required)" : "(optional)";
|
|
237
|
+
lines.push(` - "${key}": ${field.type} ${required}${field.description ? ` - ${field.description}` : ""}`);
|
|
238
|
+
}
|
|
239
|
+
return lines.join("\n");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/extractor.ts
|
|
243
|
+
var MAX_REPAIR_ATTEMPTS = 3;
|
|
244
|
+
function unwrapNamedResponse(data, schemaName) {
|
|
245
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data) && schemaName in data && typeof data[schemaName] === "object") {
|
|
246
|
+
return data[schemaName];
|
|
247
|
+
}
|
|
248
|
+
return data;
|
|
249
|
+
}
|
|
250
|
+
async function extract(input, schema, options = {}) {
|
|
251
|
+
const config = {
|
|
252
|
+
apiKey: options.apiKey || process.env.OPENROUTER_API_KEY || "",
|
|
253
|
+
model: options.model || "openrouter/free",
|
|
254
|
+
temperature: options.temperature ?? 0,
|
|
255
|
+
timeout: options.timeout ?? 3e4
|
|
256
|
+
};
|
|
257
|
+
if (!config.apiKey || config.apiKey.trim() === "") {
|
|
258
|
+
throw new Error(
|
|
259
|
+
"OpenRouter API key required. Pass it in options or set OPENROUTER_API_KEY environment variable."
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const schemaDescription = schemaToZodishString(schema);
|
|
263
|
+
const exampleJSON = schemaToPrompt(schema);
|
|
264
|
+
const userPrompt = `Extract structured data from the following text.
|
|
265
|
+
|
|
266
|
+
${schemaDescription}
|
|
267
|
+
|
|
268
|
+
Return ONLY valid JSON matching this structure:
|
|
269
|
+
${exampleJSON}
|
|
270
|
+
|
|
271
|
+
Text to extract from:
|
|
272
|
+
"""
|
|
273
|
+
${input}
|
|
274
|
+
"""`;
|
|
275
|
+
const schemaFields = {};
|
|
276
|
+
for (const [key, field] of Object.entries(schema.fields)) {
|
|
277
|
+
schemaFields[key] = { type: field.type, required: field.required };
|
|
278
|
+
}
|
|
279
|
+
let lastRaw = "";
|
|
280
|
+
let lastError = "";
|
|
281
|
+
for (let attempt = 0; attempt <= MAX_REPAIR_ATTEMPTS; attempt++) {
|
|
282
|
+
const prompt = attempt === 0 ? userPrompt : `${userPrompt}
|
|
283
|
+
|
|
284
|
+
IMPORTANT: Your previous response was invalid JSON. Here was the error:
|
|
285
|
+
${lastError}
|
|
286
|
+
|
|
287
|
+
Previous raw response:
|
|
288
|
+
${lastRaw}
|
|
289
|
+
|
|
290
|
+
Fix the JSON and return ONLY valid JSON. No explanations, no markdown, just the raw JSON object.`;
|
|
291
|
+
const response = await callLLM(prompt, config);
|
|
292
|
+
lastRaw = response.content;
|
|
293
|
+
const extracted = extractJSON(response.content);
|
|
294
|
+
if (!extracted) {
|
|
295
|
+
lastError = "Could not extract JSON from response";
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const parsed = tryParseJSON(extracted);
|
|
299
|
+
if (!parsed.success) {
|
|
300
|
+
lastError = parsed.error;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const repaired = repairJSON(extracted);
|
|
304
|
+
const finalData = repaired ? tryParseJSON(repaired).success ? JSON.parse(repaired) : parsed.data : parsed.data;
|
|
305
|
+
const unwrapped = unwrapNamedResponse(finalData, schema.name);
|
|
306
|
+
const validation = validateAgainstSchema(unwrapped, schemaFields);
|
|
307
|
+
if (!validation.valid) {
|
|
308
|
+
lastError = `Schema validation failed: ${validation.errors.join(", ")}`;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
data: unwrapped,
|
|
313
|
+
raw: response.content,
|
|
314
|
+
model: response.model,
|
|
315
|
+
usage: response.usage
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Failed to extract valid JSON after ${MAX_REPAIR_ATTEMPTS + 1} attempts. Last error: ${lastError}`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
exports.defineSchema = defineSchema;
|
|
324
|
+
exports.extract = extract;
|
|
325
|
+
exports.extractJSON = extractJSON;
|
|
326
|
+
exports.repairJSON = repairJSON;
|
|
327
|
+
exports.schemaToPrompt = schemaToPrompt;
|
|
328
|
+
exports.schemaToZodishString = schemaToZodishString;
|
|
329
|
+
exports.tryParseJSON = tryParseJSON;
|
|
330
|
+
exports.validateAgainstSchema = validateAgainstSchema;
|
|
331
|
+
//# sourceMappingURL=index.cjs.map
|
|
332
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/llm-client.ts","../src/validator.ts","../src/schema.ts","../src/extractor.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["export interface LLMClientConfig {\n apiKey: string;\n model: string;\n temperature?: number;\n timeout?: number;\n}\n\nexport interface LLMResponse {\n content: string;\n model: string;\n usage?: {\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n };\n}\n\ninterface OpenRouterChoice {\n message?: {\n content?: string;\n };\n}\n\ninterface OpenRouterResponse {\n choices?: OpenRouterChoice[];\n model?: string;\n usage?: {\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n };\n}\n\nconst DEFAULT_MODEL = \"openrouter/free\";\nconst DEFAULT_TEMPERATURE = 0;\nconst DEFAULT_TIMEOUT = 60000;\n\nexport async function callLLM(\n prompt: string,\n config: LLMClientConfig\n): Promise<LLMResponse> {\n const {\n apiKey,\n model = DEFAULT_MODEL,\n temperature = DEFAULT_TEMPERATURE,\n timeout = DEFAULT_TIMEOUT,\n } = config;\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n try {\n const response = await fetch(\"https://openrouter.ai/api/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n \"HTTP-Referer\": \"https://github.com/harshitduggal/structured-llm\",\n \"X-Title\": \"structured-llm\",\n },\n body: JSON.stringify({\n model,\n messages: [\n {\n role: \"system\",\n content: \"You are a precise data extraction assistant. You MUST respond with valid JSON only. No explanations, no markdown, no text outside the JSON. Just the raw JSON object.\",\n },\n {\n role: \"user\",\n content: prompt,\n },\n ],\n temperature,\n response_format: { type: \"json_object\" },\n }),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n const errorBody = await response.text();\n throw new Error(\n `OpenRouter API error: ${response.status} - ${errorBody}`\n );\n }\n\n const data = (await response.json()) as OpenRouterResponse;\n\n if (!data.choices || data.choices.length === 0) {\n throw new Error(\"No response from LLM\");\n }\n\n const content = data.choices[0].message?.content;\n if (!content) {\n throw new Error(\"Empty response from LLM\");\n }\n\n return {\n content,\n model: data.model || model,\n usage: data.usage\n ? {\n promptTokens: data.usage.prompt_tokens,\n completionTokens: data.usage.completion_tokens,\n totalTokens: data.usage.total_tokens,\n }\n : undefined,\n };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n","export function tryParseJSON(raw: string): { success: true; data: unknown } | { success: false; error: string } {\n try {\n const data = JSON.parse(raw);\n return { success: true, data };\n } catch (e) {\n return { success: false, error: e instanceof Error ? e.message : \"Unknown parse error\" };\n }\n}\n\nexport function extractJSON(raw: string): string | null {\n const trimmed = raw.trim();\n\n if (tryParseJSON(trimmed).success) {\n return trimmed;\n }\n\n const codeBlockMatch = trimmed.match(/```(?:json)?\\s*\\n?([\\s\\S]*?)\\n?\\s*```/);\n if (codeBlockMatch) {\n const inner = codeBlockMatch[1].trim();\n if (tryParseJSON(inner).success) {\n return inner;\n }\n }\n\n const firstBrace = trimmed.indexOf(\"{\");\n const lastBrace = trimmed.lastIndexOf(\"}\");\n if (firstBrace !== -1 && lastBrace > firstBrace) {\n const candidate = trimmed.slice(firstBrace, lastBrace + 1);\n if (tryParseJSON(candidate).success) {\n return candidate;\n }\n }\n\n const firstBracket = trimmed.indexOf(\"[\");\n const lastBracket = trimmed.lastIndexOf(\"]\");\n if (firstBracket !== -1 && lastBracket > firstBracket) {\n const candidate = trimmed.slice(firstBracket, lastBracket + 1);\n if (tryParseJSON(candidate).success) {\n return candidate;\n }\n }\n\n return null;\n}\n\nexport function repairJSON(raw: string): string | null {\n let candidate = raw.trim();\n\n candidate = candidate.replace(/,\\s*([}\\]])/g, \"$1\");\n\n candidate = candidate.replace(/'/g, '\"');\n\n candidate = candidate.replace(/(\\w+)\\s*:/g, '\"$1\":');\n\n candidate = candidate.replace(/:\\s*\"([^\"]*?)\"/g, (match, content) => {\n if (content.includes('\"')) {\n return match;\n }\n return match;\n });\n\n if (!candidate.startsWith(\"{\") && !candidate.startsWith(\"[\")) {\n const firstBrace = candidate.indexOf(\"{\");\n const firstBracket = candidate.indexOf(\"[\");\n if (firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket)) {\n candidate = candidate.slice(firstBrace);\n } else if (firstBracket !== -1) {\n candidate = candidate.slice(firstBracket);\n }\n }\n\n if (!candidate.endsWith(\"}\") && !candidate.endsWith(\"]\")) {\n const lastBrace = candidate.lastIndexOf(\"}\");\n const lastBracket = candidate.lastIndexOf(\"]\");\n if (lastBrace !== -1 && (lastBracket === -1 || lastBrace > lastBracket)) {\n candidate = candidate.slice(0, lastBrace + 1);\n } else if (lastBracket !== -1) {\n candidate = candidate.slice(0, lastBracket + 1);\n }\n }\n\n const result = tryParseJSON(candidate);\n if (result.success) {\n return candidate;\n }\n\n return null;\n}\n\nexport function validateAgainstSchema(data: unknown, schema: Record<string, { type: string; required?: boolean }>): { valid: true; data: unknown } | { valid: false; errors: string[] } {\n if (typeof data !== \"object\" || data === null || Array.isArray(data)) {\n return { valid: false, errors: [\"Root value must be an object\"] };\n }\n\n const errors: string[] = [];\n const obj = data as Record<string, unknown>;\n\n for (const [key, field] of Object.entries(schema)) {\n const value = obj[key];\n\n if (value === undefined && field.required !== false) {\n errors.push(`Missing required field: \"${key}\"`);\n continue;\n }\n\n if (value === undefined) {\n continue;\n }\n\n switch (field.type) {\n case \"string\":\n if (typeof value !== \"string\") {\n errors.push(`Field \"${key}\" must be string, got ${typeof value}`);\n }\n break;\n case \"number\":\n if (typeof value !== \"number\") {\n errors.push(`Field \"${key}\" must be number, got ${typeof value}`);\n }\n break;\n case \"boolean\":\n if (typeof value !== \"boolean\") {\n errors.push(`Field \"${key}\" must be boolean, got ${typeof value}`);\n }\n break;\n case \"array\":\n if (!Array.isArray(value)) {\n errors.push(`Field \"${key}\" must be array, got ${typeof value}`);\n }\n break;\n case \"object\":\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n errors.push(`Field \"${key}\" must be object, got ${typeof value}`);\n }\n break;\n }\n }\n\n if (errors.length > 0) {\n return { valid: false, errors };\n }\n\n return { valid: true, data };\n}\n","import { ExtractionSchema, SchemaField } from \"./types\";\n\nexport function defineSchema(name: string, fields: Record<string, SchemaField>): ExtractionSchema {\n return { name, fields };\n}\n\nexport function schemaToPrompt(schema: ExtractionSchema): string {\n const fieldDescriptions = Object.entries(schema.fields)\n .map(([key, field]) => {\n const parts = [`\"${key}\": ${fieldTypeToExample(field)}`];\n if (field.description) {\n parts.push(`// ${field.description}`);\n }\n return parts.join(\" \");\n })\n .join(\",\\n \");\n\n return `{\n \"${schema.name}\": {\n ${fieldDescriptions}\n }\n}`;\n}\n\nfunction fieldTypeToExample(field: SchemaField): string {\n switch (field.type) {\n case \"string\":\n return `\"...\"`;\n case \"number\":\n return \"0\";\n case \"boolean\":\n return \"true\";\n case \"array\":\n if (field.items) {\n return `[${fieldTypeToExample(field.items)}]`;\n }\n return \"[]\";\n case \"object\":\n if (field.properties) {\n const inner = Object.entries(field.properties)\n .map(([k, v]) => `\"${k}\": ${fieldTypeToExample(v)}`)\n .join(\", \");\n return `{ ${inner} }`;\n }\n return \"{}\";\n }\n}\n\nexport function schemaToZodishString(schema: ExtractionSchema): string {\n const lines: string[] = [];\n lines.push(`Expected JSON structure for \"${schema.name}\":`);\n\n for (const [key, field] of Object.entries(schema.fields)) {\n const required = field.required !== false ? \"(required)\" : \"(optional)\";\n lines.push(` - \"${key}\": ${field.type} ${required}${field.description ? ` - ${field.description}` : \"\"}`);\n }\n\n return lines.join(\"\\n\");\n}\n","import { ExtractionSchema, ExtractionOptions, ExtractionResult } from \"./types\";\nimport { callLLM, LLMClientConfig } from \"./llm-client\";\nimport { extractJSON, repairJSON, tryParseJSON, validateAgainstSchema } from \"./validator\";\nimport { schemaToPrompt, schemaToZodishString } from \"./schema\";\n\n\nconst MAX_REPAIR_ATTEMPTS = 3;\n\nfunction unwrapNamedResponse(data: unknown, schemaName: string): unknown {\n if (\n typeof data === \"object\" &&\n data !== null &&\n !Array.isArray(data) &&\n schemaName in data &&\n typeof (data as Record<string, unknown>)[schemaName] === \"object\"\n ) {\n return (data as Record<string, unknown>)[schemaName];\n }\n return data;\n}\n\nexport async function extract<T = Record<string, unknown>>(\n input: string,\n schema: ExtractionSchema,\n options: ExtractionOptions = {}\n): Promise<ExtractionResult<T>> {\n const config: LLMClientConfig = {\n apiKey: options.apiKey || process.env.OPENROUTER_API_KEY || \"\",\n model: options.model || \"openrouter/free\",\n temperature: options.temperature ?? 0,\n timeout: options.timeout ?? 30000,\n };\n\n if (!config.apiKey || config.apiKey.trim() === \"\") {\n throw new Error(\n \"OpenRouter API key required. Pass it in options or set OPENROUTER_API_KEY environment variable.\"\n );\n }\n\n const schemaDescription = schemaToZodishString(schema);\n const exampleJSON = schemaToPrompt(schema);\n\n const userPrompt = `Extract structured data from the following text.\n\n${schemaDescription}\n\nReturn ONLY valid JSON matching this structure:\n${exampleJSON}\n\nText to extract from:\n\"\"\"\n${input}\n\"\"\"`;\n\n const schemaFields: Record<string, { type: string; required?: boolean }> = {};\n for (const [key, field] of Object.entries(schema.fields)) {\n schemaFields[key] = { type: field.type, required: field.required };\n }\n\n let lastRaw = \"\";\n let lastError = \"\";\n\n for (let attempt = 0; attempt <= MAX_REPAIR_ATTEMPTS; attempt++) {\n const prompt =\n attempt === 0\n ? userPrompt\n : `${userPrompt}\n\nIMPORTANT: Your previous response was invalid JSON. Here was the error:\n${lastError}\n\nPrevious raw response:\n${lastRaw}\n\nFix the JSON and return ONLY valid JSON. No explanations, no markdown, just the raw JSON object.`;\n\n const response = await callLLM(prompt, config);\n lastRaw = response.content;\n\n const extracted = extractJSON(response.content);\n if (!extracted) {\n lastError = \"Could not extract JSON from response\";\n continue;\n }\n\n const parsed = tryParseJSON(extracted);\n if (!parsed.success) {\n lastError = parsed.error;\n continue;\n }\n\n const repaired = repairJSON(extracted);\n const finalData = repaired\n ? tryParseJSON(repaired).success\n ? JSON.parse(repaired)\n : parsed.data\n : parsed.data;\n\n const unwrapped = unwrapNamedResponse(finalData, schema.name);\n\n const validation = validateAgainstSchema(unwrapped, schemaFields);\n if (!validation.valid) {\n lastError = `Schema validation failed: ${validation.errors.join(\", \")}`;\n continue;\n }\n\n return {\n data: unwrapped as T,\n raw: response.content,\n model: response.model,\n usage: response.usage,\n };\n }\n\n throw new Error(\n `Failed to extract valid JSON after ${MAX_REPAIR_ATTEMPTS + 1} attempts. Last error: ${lastError}`\n );\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
interface ExtractionSchema {
|
|
2
|
+
name: string;
|
|
3
|
+
fields: Record<string, SchemaField>;
|
|
4
|
+
}
|
|
5
|
+
type SchemaField = {
|
|
6
|
+
type: "string" | "number" | "boolean" | "array" | "object";
|
|
7
|
+
description?: string;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
items?: SchemaField;
|
|
10
|
+
properties?: Record<string, SchemaField>;
|
|
11
|
+
};
|
|
12
|
+
interface ExtractionOptions {
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
temperature?: number;
|
|
16
|
+
maxRetries?: number;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
}
|
|
19
|
+
interface ExtractionResult<T> {
|
|
20
|
+
data: T;
|
|
21
|
+
raw: string;
|
|
22
|
+
model: string;
|
|
23
|
+
usage?: {
|
|
24
|
+
promptTokens: number;
|
|
25
|
+
completionTokens: number;
|
|
26
|
+
totalTokens: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
interface ExtractionError {
|
|
30
|
+
type: "validation" | "api" | "timeout" | "parse";
|
|
31
|
+
message: string;
|
|
32
|
+
raw?: string;
|
|
33
|
+
attempts: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
declare function extract<T = Record<string, unknown>>(input: string, schema: ExtractionSchema, options?: ExtractionOptions): Promise<ExtractionResult<T>>;
|
|
37
|
+
|
|
38
|
+
declare function defineSchema(name: string, fields: Record<string, SchemaField>): ExtractionSchema;
|
|
39
|
+
declare function schemaToPrompt(schema: ExtractionSchema): string;
|
|
40
|
+
declare function schemaToZodishString(schema: ExtractionSchema): string;
|
|
41
|
+
|
|
42
|
+
declare function tryParseJSON(raw: string): {
|
|
43
|
+
success: true;
|
|
44
|
+
data: unknown;
|
|
45
|
+
} | {
|
|
46
|
+
success: false;
|
|
47
|
+
error: string;
|
|
48
|
+
};
|
|
49
|
+
declare function extractJSON(raw: string): string | null;
|
|
50
|
+
declare function repairJSON(raw: string): string | null;
|
|
51
|
+
declare function validateAgainstSchema(data: unknown, schema: Record<string, {
|
|
52
|
+
type: string;
|
|
53
|
+
required?: boolean;
|
|
54
|
+
}>): {
|
|
55
|
+
valid: true;
|
|
56
|
+
data: unknown;
|
|
57
|
+
} | {
|
|
58
|
+
valid: false;
|
|
59
|
+
errors: string[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export { type ExtractionError, type ExtractionOptions, type ExtractionResult, type ExtractionSchema, type SchemaField, defineSchema, extract, extractJSON, repairJSON, schemaToPrompt, schemaToZodishString, tryParseJSON, validateAgainstSchema };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
interface ExtractionSchema {
|
|
2
|
+
name: string;
|
|
3
|
+
fields: Record<string, SchemaField>;
|
|
4
|
+
}
|
|
5
|
+
type SchemaField = {
|
|
6
|
+
type: "string" | "number" | "boolean" | "array" | "object";
|
|
7
|
+
description?: string;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
items?: SchemaField;
|
|
10
|
+
properties?: Record<string, SchemaField>;
|
|
11
|
+
};
|
|
12
|
+
interface ExtractionOptions {
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
temperature?: number;
|
|
16
|
+
maxRetries?: number;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
}
|
|
19
|
+
interface ExtractionResult<T> {
|
|
20
|
+
data: T;
|
|
21
|
+
raw: string;
|
|
22
|
+
model: string;
|
|
23
|
+
usage?: {
|
|
24
|
+
promptTokens: number;
|
|
25
|
+
completionTokens: number;
|
|
26
|
+
totalTokens: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
interface ExtractionError {
|
|
30
|
+
type: "validation" | "api" | "timeout" | "parse";
|
|
31
|
+
message: string;
|
|
32
|
+
raw?: string;
|
|
33
|
+
attempts: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
declare function extract<T = Record<string, unknown>>(input: string, schema: ExtractionSchema, options?: ExtractionOptions): Promise<ExtractionResult<T>>;
|
|
37
|
+
|
|
38
|
+
declare function defineSchema(name: string, fields: Record<string, SchemaField>): ExtractionSchema;
|
|
39
|
+
declare function schemaToPrompt(schema: ExtractionSchema): string;
|
|
40
|
+
declare function schemaToZodishString(schema: ExtractionSchema): string;
|
|
41
|
+
|
|
42
|
+
declare function tryParseJSON(raw: string): {
|
|
43
|
+
success: true;
|
|
44
|
+
data: unknown;
|
|
45
|
+
} | {
|
|
46
|
+
success: false;
|
|
47
|
+
error: string;
|
|
48
|
+
};
|
|
49
|
+
declare function extractJSON(raw: string): string | null;
|
|
50
|
+
declare function repairJSON(raw: string): string | null;
|
|
51
|
+
declare function validateAgainstSchema(data: unknown, schema: Record<string, {
|
|
52
|
+
type: string;
|
|
53
|
+
required?: boolean;
|
|
54
|
+
}>): {
|
|
55
|
+
valid: true;
|
|
56
|
+
data: unknown;
|
|
57
|
+
} | {
|
|
58
|
+
valid: false;
|
|
59
|
+
errors: string[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export { type ExtractionError, type ExtractionOptions, type ExtractionResult, type ExtractionSchema, type SchemaField, defineSchema, extract, extractJSON, repairJSON, schemaToPrompt, schemaToZodishString, tryParseJSON, validateAgainstSchema };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// src/llm-client.ts
|
|
2
|
+
var DEFAULT_MODEL = "openrouter/free";
|
|
3
|
+
var DEFAULT_TEMPERATURE = 0;
|
|
4
|
+
var DEFAULT_TIMEOUT = 6e4;
|
|
5
|
+
async function callLLM(prompt, config) {
|
|
6
|
+
const {
|
|
7
|
+
apiKey,
|
|
8
|
+
model = DEFAULT_MODEL,
|
|
9
|
+
temperature = DEFAULT_TEMPERATURE,
|
|
10
|
+
timeout = DEFAULT_TIMEOUT
|
|
11
|
+
} = config;
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
Authorization: `Bearer ${apiKey}`,
|
|
20
|
+
"HTTP-Referer": "https://github.com/harshitduggal/structured-llm",
|
|
21
|
+
"X-Title": "structured-llm"
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify({
|
|
24
|
+
model,
|
|
25
|
+
messages: [
|
|
26
|
+
{
|
|
27
|
+
role: "system",
|
|
28
|
+
content: "You are a precise data extraction assistant. You MUST respond with valid JSON only. No explanations, no markdown, no text outside the JSON. Just the raw JSON object."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
role: "user",
|
|
32
|
+
content: prompt
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
temperature,
|
|
36
|
+
response_format: { type: "json_object" }
|
|
37
|
+
}),
|
|
38
|
+
signal: controller.signal
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const errorBody = await response.text();
|
|
42
|
+
throw new Error(
|
|
43
|
+
`OpenRouter API error: ${response.status} - ${errorBody}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
if (!data.choices || data.choices.length === 0) {
|
|
48
|
+
throw new Error("No response from LLM");
|
|
49
|
+
}
|
|
50
|
+
const content = data.choices[0].message?.content;
|
|
51
|
+
if (!content) {
|
|
52
|
+
throw new Error("Empty response from LLM");
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
content,
|
|
56
|
+
model: data.model || model,
|
|
57
|
+
usage: data.usage ? {
|
|
58
|
+
promptTokens: data.usage.prompt_tokens,
|
|
59
|
+
completionTokens: data.usage.completion_tokens,
|
|
60
|
+
totalTokens: data.usage.total_tokens
|
|
61
|
+
} : void 0
|
|
62
|
+
};
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(timeoutId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/validator.ts
|
|
69
|
+
function tryParseJSON(raw) {
|
|
70
|
+
try {
|
|
71
|
+
const data = JSON.parse(raw);
|
|
72
|
+
return { success: true, data };
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return { success: false, error: e instanceof Error ? e.message : "Unknown parse error" };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function extractJSON(raw) {
|
|
78
|
+
const trimmed = raw.trim();
|
|
79
|
+
if (tryParseJSON(trimmed).success) {
|
|
80
|
+
return trimmed;
|
|
81
|
+
}
|
|
82
|
+
const codeBlockMatch = trimmed.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
83
|
+
if (codeBlockMatch) {
|
|
84
|
+
const inner = codeBlockMatch[1].trim();
|
|
85
|
+
if (tryParseJSON(inner).success) {
|
|
86
|
+
return inner;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const firstBrace = trimmed.indexOf("{");
|
|
90
|
+
const lastBrace = trimmed.lastIndexOf("}");
|
|
91
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
92
|
+
const candidate = trimmed.slice(firstBrace, lastBrace + 1);
|
|
93
|
+
if (tryParseJSON(candidate).success) {
|
|
94
|
+
return candidate;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const firstBracket = trimmed.indexOf("[");
|
|
98
|
+
const lastBracket = trimmed.lastIndexOf("]");
|
|
99
|
+
if (firstBracket !== -1 && lastBracket > firstBracket) {
|
|
100
|
+
const candidate = trimmed.slice(firstBracket, lastBracket + 1);
|
|
101
|
+
if (tryParseJSON(candidate).success) {
|
|
102
|
+
return candidate;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function repairJSON(raw) {
|
|
108
|
+
let candidate = raw.trim();
|
|
109
|
+
candidate = candidate.replace(/,\s*([}\]])/g, "$1");
|
|
110
|
+
candidate = candidate.replace(/'/g, '"');
|
|
111
|
+
candidate = candidate.replace(/(\w+)\s*:/g, '"$1":');
|
|
112
|
+
candidate = candidate.replace(/:\s*"([^"]*?)"/g, (match, content) => {
|
|
113
|
+
if (content.includes('"')) {
|
|
114
|
+
return match;
|
|
115
|
+
}
|
|
116
|
+
return match;
|
|
117
|
+
});
|
|
118
|
+
if (!candidate.startsWith("{") && !candidate.startsWith("[")) {
|
|
119
|
+
const firstBrace = candidate.indexOf("{");
|
|
120
|
+
const firstBracket = candidate.indexOf("[");
|
|
121
|
+
if (firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket)) {
|
|
122
|
+
candidate = candidate.slice(firstBrace);
|
|
123
|
+
} else if (firstBracket !== -1) {
|
|
124
|
+
candidate = candidate.slice(firstBracket);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!candidate.endsWith("}") && !candidate.endsWith("]")) {
|
|
128
|
+
const lastBrace = candidate.lastIndexOf("}");
|
|
129
|
+
const lastBracket = candidate.lastIndexOf("]");
|
|
130
|
+
if (lastBrace !== -1 && (lastBracket === -1 || lastBrace > lastBracket)) {
|
|
131
|
+
candidate = candidate.slice(0, lastBrace + 1);
|
|
132
|
+
} else if (lastBracket !== -1) {
|
|
133
|
+
candidate = candidate.slice(0, lastBracket + 1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const result = tryParseJSON(candidate);
|
|
137
|
+
if (result.success) {
|
|
138
|
+
return candidate;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
function validateAgainstSchema(data, schema) {
|
|
143
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
144
|
+
return { valid: false, errors: ["Root value must be an object"] };
|
|
145
|
+
}
|
|
146
|
+
const errors = [];
|
|
147
|
+
const obj = data;
|
|
148
|
+
for (const [key, field] of Object.entries(schema)) {
|
|
149
|
+
const value = obj[key];
|
|
150
|
+
if (value === void 0 && field.required !== false) {
|
|
151
|
+
errors.push(`Missing required field: "${key}"`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (value === void 0) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
switch (field.type) {
|
|
158
|
+
case "string":
|
|
159
|
+
if (typeof value !== "string") {
|
|
160
|
+
errors.push(`Field "${key}" must be string, got ${typeof value}`);
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
case "number":
|
|
164
|
+
if (typeof value !== "number") {
|
|
165
|
+
errors.push(`Field "${key}" must be number, got ${typeof value}`);
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
case "boolean":
|
|
169
|
+
if (typeof value !== "boolean") {
|
|
170
|
+
errors.push(`Field "${key}" must be boolean, got ${typeof value}`);
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
case "array":
|
|
174
|
+
if (!Array.isArray(value)) {
|
|
175
|
+
errors.push(`Field "${key}" must be array, got ${typeof value}`);
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
case "object":
|
|
179
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
180
|
+
errors.push(`Field "${key}" must be object, got ${typeof value}`);
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (errors.length > 0) {
|
|
186
|
+
return { valid: false, errors };
|
|
187
|
+
}
|
|
188
|
+
return { valid: true, data };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/schema.ts
|
|
192
|
+
function defineSchema(name, fields) {
|
|
193
|
+
return { name, fields };
|
|
194
|
+
}
|
|
195
|
+
function schemaToPrompt(schema) {
|
|
196
|
+
const fieldDescriptions = Object.entries(schema.fields).map(([key, field]) => {
|
|
197
|
+
const parts = [`"${key}": ${fieldTypeToExample(field)}`];
|
|
198
|
+
if (field.description) {
|
|
199
|
+
parts.push(`// ${field.description}`);
|
|
200
|
+
}
|
|
201
|
+
return parts.join(" ");
|
|
202
|
+
}).join(",\n ");
|
|
203
|
+
return `{
|
|
204
|
+
"${schema.name}": {
|
|
205
|
+
${fieldDescriptions}
|
|
206
|
+
}
|
|
207
|
+
}`;
|
|
208
|
+
}
|
|
209
|
+
function fieldTypeToExample(field) {
|
|
210
|
+
switch (field.type) {
|
|
211
|
+
case "string":
|
|
212
|
+
return `"..."`;
|
|
213
|
+
case "number":
|
|
214
|
+
return "0";
|
|
215
|
+
case "boolean":
|
|
216
|
+
return "true";
|
|
217
|
+
case "array":
|
|
218
|
+
if (field.items) {
|
|
219
|
+
return `[${fieldTypeToExample(field.items)}]`;
|
|
220
|
+
}
|
|
221
|
+
return "[]";
|
|
222
|
+
case "object":
|
|
223
|
+
if (field.properties) {
|
|
224
|
+
const inner = Object.entries(field.properties).map(([k, v]) => `"${k}": ${fieldTypeToExample(v)}`).join(", ");
|
|
225
|
+
return `{ ${inner} }`;
|
|
226
|
+
}
|
|
227
|
+
return "{}";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function schemaToZodishString(schema) {
|
|
231
|
+
const lines = [];
|
|
232
|
+
lines.push(`Expected JSON structure for "${schema.name}":`);
|
|
233
|
+
for (const [key, field] of Object.entries(schema.fields)) {
|
|
234
|
+
const required = field.required !== false ? "(required)" : "(optional)";
|
|
235
|
+
lines.push(` - "${key}": ${field.type} ${required}${field.description ? ` - ${field.description}` : ""}`);
|
|
236
|
+
}
|
|
237
|
+
return lines.join("\n");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/extractor.ts
|
|
241
|
+
var MAX_REPAIR_ATTEMPTS = 3;
|
|
242
|
+
function unwrapNamedResponse(data, schemaName) {
|
|
243
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data) && schemaName in data && typeof data[schemaName] === "object") {
|
|
244
|
+
return data[schemaName];
|
|
245
|
+
}
|
|
246
|
+
return data;
|
|
247
|
+
}
|
|
248
|
+
async function extract(input, schema, options = {}) {
|
|
249
|
+
const config = {
|
|
250
|
+
apiKey: options.apiKey || process.env.OPENROUTER_API_KEY || "",
|
|
251
|
+
model: options.model || "openrouter/free",
|
|
252
|
+
temperature: options.temperature ?? 0,
|
|
253
|
+
timeout: options.timeout ?? 3e4
|
|
254
|
+
};
|
|
255
|
+
if (!config.apiKey || config.apiKey.trim() === "") {
|
|
256
|
+
throw new Error(
|
|
257
|
+
"OpenRouter API key required. Pass it in options or set OPENROUTER_API_KEY environment variable."
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const schemaDescription = schemaToZodishString(schema);
|
|
261
|
+
const exampleJSON = schemaToPrompt(schema);
|
|
262
|
+
const userPrompt = `Extract structured data from the following text.
|
|
263
|
+
|
|
264
|
+
${schemaDescription}
|
|
265
|
+
|
|
266
|
+
Return ONLY valid JSON matching this structure:
|
|
267
|
+
${exampleJSON}
|
|
268
|
+
|
|
269
|
+
Text to extract from:
|
|
270
|
+
"""
|
|
271
|
+
${input}
|
|
272
|
+
"""`;
|
|
273
|
+
const schemaFields = {};
|
|
274
|
+
for (const [key, field] of Object.entries(schema.fields)) {
|
|
275
|
+
schemaFields[key] = { type: field.type, required: field.required };
|
|
276
|
+
}
|
|
277
|
+
let lastRaw = "";
|
|
278
|
+
let lastError = "";
|
|
279
|
+
for (let attempt = 0; attempt <= MAX_REPAIR_ATTEMPTS; attempt++) {
|
|
280
|
+
const prompt = attempt === 0 ? userPrompt : `${userPrompt}
|
|
281
|
+
|
|
282
|
+
IMPORTANT: Your previous response was invalid JSON. Here was the error:
|
|
283
|
+
${lastError}
|
|
284
|
+
|
|
285
|
+
Previous raw response:
|
|
286
|
+
${lastRaw}
|
|
287
|
+
|
|
288
|
+
Fix the JSON and return ONLY valid JSON. No explanations, no markdown, just the raw JSON object.`;
|
|
289
|
+
const response = await callLLM(prompt, config);
|
|
290
|
+
lastRaw = response.content;
|
|
291
|
+
const extracted = extractJSON(response.content);
|
|
292
|
+
if (!extracted) {
|
|
293
|
+
lastError = "Could not extract JSON from response";
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const parsed = tryParseJSON(extracted);
|
|
297
|
+
if (!parsed.success) {
|
|
298
|
+
lastError = parsed.error;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const repaired = repairJSON(extracted);
|
|
302
|
+
const finalData = repaired ? tryParseJSON(repaired).success ? JSON.parse(repaired) : parsed.data : parsed.data;
|
|
303
|
+
const unwrapped = unwrapNamedResponse(finalData, schema.name);
|
|
304
|
+
const validation = validateAgainstSchema(unwrapped, schemaFields);
|
|
305
|
+
if (!validation.valid) {
|
|
306
|
+
lastError = `Schema validation failed: ${validation.errors.join(", ")}`;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
data: unwrapped,
|
|
311
|
+
raw: response.content,
|
|
312
|
+
model: response.model,
|
|
313
|
+
usage: response.usage
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
throw new Error(
|
|
317
|
+
`Failed to extract valid JSON after ${MAX_REPAIR_ATTEMPTS + 1} attempts. Last error: ${lastError}`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export { defineSchema, extract, extractJSON, repairJSON, schemaToPrompt, schemaToZodishString, tryParseJSON, validateAgainstSchema };
|
|
322
|
+
//# sourceMappingURL=index.js.map
|
|
323
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/llm-client.ts","../src/validator.ts","../src/schema.ts","../src/extractor.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export interface LLMClientConfig {\n apiKey: string;\n model: string;\n temperature?: number;\n timeout?: number;\n}\n\nexport interface LLMResponse {\n content: string;\n model: string;\n usage?: {\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n };\n}\n\ninterface OpenRouterChoice {\n message?: {\n content?: string;\n };\n}\n\ninterface OpenRouterResponse {\n choices?: OpenRouterChoice[];\n model?: string;\n usage?: {\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n };\n}\n\nconst DEFAULT_MODEL = \"openrouter/free\";\nconst DEFAULT_TEMPERATURE = 0;\nconst DEFAULT_TIMEOUT = 60000;\n\nexport async function callLLM(\n prompt: string,\n config: LLMClientConfig\n): Promise<LLMResponse> {\n const {\n apiKey,\n model = DEFAULT_MODEL,\n temperature = DEFAULT_TEMPERATURE,\n timeout = DEFAULT_TIMEOUT,\n } = config;\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n try {\n const response = await fetch(\"https://openrouter.ai/api/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n \"HTTP-Referer\": \"https://github.com/harshitduggal/structured-llm\",\n \"X-Title\": \"structured-llm\",\n },\n body: JSON.stringify({\n model,\n messages: [\n {\n role: \"system\",\n content: \"You are a precise data extraction assistant. You MUST respond with valid JSON only. No explanations, no markdown, no text outside the JSON. Just the raw JSON object.\",\n },\n {\n role: \"user\",\n content: prompt,\n },\n ],\n temperature,\n response_format: { type: \"json_object\" },\n }),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n const errorBody = await response.text();\n throw new Error(\n `OpenRouter API error: ${response.status} - ${errorBody}`\n );\n }\n\n const data = (await response.json()) as OpenRouterResponse;\n\n if (!data.choices || data.choices.length === 0) {\n throw new Error(\"No response from LLM\");\n }\n\n const content = data.choices[0].message?.content;\n if (!content) {\n throw new Error(\"Empty response from LLM\");\n }\n\n return {\n content,\n model: data.model || model,\n usage: data.usage\n ? {\n promptTokens: data.usage.prompt_tokens,\n completionTokens: data.usage.completion_tokens,\n totalTokens: data.usage.total_tokens,\n }\n : undefined,\n };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n","export function tryParseJSON(raw: string): { success: true; data: unknown } | { success: false; error: string } {\n try {\n const data = JSON.parse(raw);\n return { success: true, data };\n } catch (e) {\n return { success: false, error: e instanceof Error ? e.message : \"Unknown parse error\" };\n }\n}\n\nexport function extractJSON(raw: string): string | null {\n const trimmed = raw.trim();\n\n if (tryParseJSON(trimmed).success) {\n return trimmed;\n }\n\n const codeBlockMatch = trimmed.match(/```(?:json)?\\s*\\n?([\\s\\S]*?)\\n?\\s*```/);\n if (codeBlockMatch) {\n const inner = codeBlockMatch[1].trim();\n if (tryParseJSON(inner).success) {\n return inner;\n }\n }\n\n const firstBrace = trimmed.indexOf(\"{\");\n const lastBrace = trimmed.lastIndexOf(\"}\");\n if (firstBrace !== -1 && lastBrace > firstBrace) {\n const candidate = trimmed.slice(firstBrace, lastBrace + 1);\n if (tryParseJSON(candidate).success) {\n return candidate;\n }\n }\n\n const firstBracket = trimmed.indexOf(\"[\");\n const lastBracket = trimmed.lastIndexOf(\"]\");\n if (firstBracket !== -1 && lastBracket > firstBracket) {\n const candidate = trimmed.slice(firstBracket, lastBracket + 1);\n if (tryParseJSON(candidate).success) {\n return candidate;\n }\n }\n\n return null;\n}\n\nexport function repairJSON(raw: string): string | null {\n let candidate = raw.trim();\n\n candidate = candidate.replace(/,\\s*([}\\]])/g, \"$1\");\n\n candidate = candidate.replace(/'/g, '\"');\n\n candidate = candidate.replace(/(\\w+)\\s*:/g, '\"$1\":');\n\n candidate = candidate.replace(/:\\s*\"([^\"]*?)\"/g, (match, content) => {\n if (content.includes('\"')) {\n return match;\n }\n return match;\n });\n\n if (!candidate.startsWith(\"{\") && !candidate.startsWith(\"[\")) {\n const firstBrace = candidate.indexOf(\"{\");\n const firstBracket = candidate.indexOf(\"[\");\n if (firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket)) {\n candidate = candidate.slice(firstBrace);\n } else if (firstBracket !== -1) {\n candidate = candidate.slice(firstBracket);\n }\n }\n\n if (!candidate.endsWith(\"}\") && !candidate.endsWith(\"]\")) {\n const lastBrace = candidate.lastIndexOf(\"}\");\n const lastBracket = candidate.lastIndexOf(\"]\");\n if (lastBrace !== -1 && (lastBracket === -1 || lastBrace > lastBracket)) {\n candidate = candidate.slice(0, lastBrace + 1);\n } else if (lastBracket !== -1) {\n candidate = candidate.slice(0, lastBracket + 1);\n }\n }\n\n const result = tryParseJSON(candidate);\n if (result.success) {\n return candidate;\n }\n\n return null;\n}\n\nexport function validateAgainstSchema(data: unknown, schema: Record<string, { type: string; required?: boolean }>): { valid: true; data: unknown } | { valid: false; errors: string[] } {\n if (typeof data !== \"object\" || data === null || Array.isArray(data)) {\n return { valid: false, errors: [\"Root value must be an object\"] };\n }\n\n const errors: string[] = [];\n const obj = data as Record<string, unknown>;\n\n for (const [key, field] of Object.entries(schema)) {\n const value = obj[key];\n\n if (value === undefined && field.required !== false) {\n errors.push(`Missing required field: \"${key}\"`);\n continue;\n }\n\n if (value === undefined) {\n continue;\n }\n\n switch (field.type) {\n case \"string\":\n if (typeof value !== \"string\") {\n errors.push(`Field \"${key}\" must be string, got ${typeof value}`);\n }\n break;\n case \"number\":\n if (typeof value !== \"number\") {\n errors.push(`Field \"${key}\" must be number, got ${typeof value}`);\n }\n break;\n case \"boolean\":\n if (typeof value !== \"boolean\") {\n errors.push(`Field \"${key}\" must be boolean, got ${typeof value}`);\n }\n break;\n case \"array\":\n if (!Array.isArray(value)) {\n errors.push(`Field \"${key}\" must be array, got ${typeof value}`);\n }\n break;\n case \"object\":\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n errors.push(`Field \"${key}\" must be object, got ${typeof value}`);\n }\n break;\n }\n }\n\n if (errors.length > 0) {\n return { valid: false, errors };\n }\n\n return { valid: true, data };\n}\n","import { ExtractionSchema, SchemaField } from \"./types\";\n\nexport function defineSchema(name: string, fields: Record<string, SchemaField>): ExtractionSchema {\n return { name, fields };\n}\n\nexport function schemaToPrompt(schema: ExtractionSchema): string {\n const fieldDescriptions = Object.entries(schema.fields)\n .map(([key, field]) => {\n const parts = [`\"${key}\": ${fieldTypeToExample(field)}`];\n if (field.description) {\n parts.push(`// ${field.description}`);\n }\n return parts.join(\" \");\n })\n .join(\",\\n \");\n\n return `{\n \"${schema.name}\": {\n ${fieldDescriptions}\n }\n}`;\n}\n\nfunction fieldTypeToExample(field: SchemaField): string {\n switch (field.type) {\n case \"string\":\n return `\"...\"`;\n case \"number\":\n return \"0\";\n case \"boolean\":\n return \"true\";\n case \"array\":\n if (field.items) {\n return `[${fieldTypeToExample(field.items)}]`;\n }\n return \"[]\";\n case \"object\":\n if (field.properties) {\n const inner = Object.entries(field.properties)\n .map(([k, v]) => `\"${k}\": ${fieldTypeToExample(v)}`)\n .join(\", \");\n return `{ ${inner} }`;\n }\n return \"{}\";\n }\n}\n\nexport function schemaToZodishString(schema: ExtractionSchema): string {\n const lines: string[] = [];\n lines.push(`Expected JSON structure for \"${schema.name}\":`);\n\n for (const [key, field] of Object.entries(schema.fields)) {\n const required = field.required !== false ? \"(required)\" : \"(optional)\";\n lines.push(` - \"${key}\": ${field.type} ${required}${field.description ? ` - ${field.description}` : \"\"}`);\n }\n\n return lines.join(\"\\n\");\n}\n","import { ExtractionSchema, ExtractionOptions, ExtractionResult } from \"./types\";\nimport { callLLM, LLMClientConfig } from \"./llm-client\";\nimport { extractJSON, repairJSON, tryParseJSON, validateAgainstSchema } from \"./validator\";\nimport { schemaToPrompt, schemaToZodishString } from \"./schema\";\n\n\nconst MAX_REPAIR_ATTEMPTS = 3;\n\nfunction unwrapNamedResponse(data: unknown, schemaName: string): unknown {\n if (\n typeof data === \"object\" &&\n data !== null &&\n !Array.isArray(data) &&\n schemaName in data &&\n typeof (data as Record<string, unknown>)[schemaName] === \"object\"\n ) {\n return (data as Record<string, unknown>)[schemaName];\n }\n return data;\n}\n\nexport async function extract<T = Record<string, unknown>>(\n input: string,\n schema: ExtractionSchema,\n options: ExtractionOptions = {}\n): Promise<ExtractionResult<T>> {\n const config: LLMClientConfig = {\n apiKey: options.apiKey || process.env.OPENROUTER_API_KEY || \"\",\n model: options.model || \"openrouter/free\",\n temperature: options.temperature ?? 0,\n timeout: options.timeout ?? 30000,\n };\n\n if (!config.apiKey || config.apiKey.trim() === \"\") {\n throw new Error(\n \"OpenRouter API key required. Pass it in options or set OPENROUTER_API_KEY environment variable.\"\n );\n }\n\n const schemaDescription = schemaToZodishString(schema);\n const exampleJSON = schemaToPrompt(schema);\n\n const userPrompt = `Extract structured data from the following text.\n\n${schemaDescription}\n\nReturn ONLY valid JSON matching this structure:\n${exampleJSON}\n\nText to extract from:\n\"\"\"\n${input}\n\"\"\"`;\n\n const schemaFields: Record<string, { type: string; required?: boolean }> = {};\n for (const [key, field] of Object.entries(schema.fields)) {\n schemaFields[key] = { type: field.type, required: field.required };\n }\n\n let lastRaw = \"\";\n let lastError = \"\";\n\n for (let attempt = 0; attempt <= MAX_REPAIR_ATTEMPTS; attempt++) {\n const prompt =\n attempt === 0\n ? userPrompt\n : `${userPrompt}\n\nIMPORTANT: Your previous response was invalid JSON. Here was the error:\n${lastError}\n\nPrevious raw response:\n${lastRaw}\n\nFix the JSON and return ONLY valid JSON. No explanations, no markdown, just the raw JSON object.`;\n\n const response = await callLLM(prompt, config);\n lastRaw = response.content;\n\n const extracted = extractJSON(response.content);\n if (!extracted) {\n lastError = \"Could not extract JSON from response\";\n continue;\n }\n\n const parsed = tryParseJSON(extracted);\n if (!parsed.success) {\n lastError = parsed.error;\n continue;\n }\n\n const repaired = repairJSON(extracted);\n const finalData = repaired\n ? tryParseJSON(repaired).success\n ? JSON.parse(repaired)\n : parsed.data\n : parsed.data;\n\n const unwrapped = unwrapNamedResponse(finalData, schema.name);\n\n const validation = validateAgainstSchema(unwrapped, schemaFields);\n if (!validation.valid) {\n lastError = `Schema validation failed: ${validation.errors.join(\", \")}`;\n continue;\n }\n\n return {\n data: unwrapped as T,\n raw: response.content,\n model: response.model,\n usage: response.usage,\n };\n }\n\n throw new Error(\n `Failed to extract valid JSON after ${MAX_REPAIR_ATTEMPTS + 1} attempts. Last error: ${lastError}`\n );\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@painitehq/structured-llm",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Force LLM output into structured, type-safe JSON. Stop your app from crashing on malformed AI responses.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "bun test",
|
|
24
|
+
"lint": "eslint src/",
|
|
25
|
+
"prepublishOnly": "bun run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"llm",
|
|
29
|
+
"ai",
|
|
30
|
+
"json",
|
|
31
|
+
"structured-output",
|
|
32
|
+
"schema",
|
|
33
|
+
"openrouter",
|
|
34
|
+
"extraction",
|
|
35
|
+
"type-safe",
|
|
36
|
+
"zod"
|
|
37
|
+
],
|
|
38
|
+
"author": "harshitduggal",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"dependencies": {},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^26.0.0",
|
|
43
|
+
"tsup": "^8.0.0",
|
|
44
|
+
"typescript": "~5.7.0",
|
|
45
|
+
"@types/bun": "latest"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"zod": "^3.23.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"zod": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|