@struktur/sdk 2.1.1 → 2.2.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/dist/index.js +4111 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers.js +492 -0
- package/dist/parsers.js.map +1 -0
- package/dist/strategies.js +2435 -0
- package/dist/strategies.js.map +1 -0
- package/package.json +25 -13
- package/src/agent-cli-integration.test.ts +0 -47
- package/src/agent-export.test.ts +0 -17
- package/src/agent-tool-labels.test.ts +0 -50
- package/src/artifacts/AGENTS.md +0 -16
- package/src/artifacts/fileToArtifact.test.ts +0 -37
- package/src/artifacts/fileToArtifact.ts +0 -44
- package/src/artifacts/input.test.ts +0 -243
- package/src/artifacts/input.ts +0 -360
- package/src/artifacts/providers.test.ts +0 -19
- package/src/artifacts/providers.ts +0 -7
- package/src/artifacts/urlToArtifact.test.ts +0 -23
- package/src/artifacts/urlToArtifact.ts +0 -19
- package/src/auth/AGENTS.md +0 -11
- package/src/auth/config.test.ts +0 -132
- package/src/auth/config.ts +0 -186
- package/src/auth/tokens.test.ts +0 -58
- package/src/auth/tokens.ts +0 -229
- package/src/chunking/AGENTS.md +0 -11
- package/src/chunking/ArtifactBatcher.test.ts +0 -22
- package/src/chunking/ArtifactBatcher.ts +0 -110
- package/src/chunking/ArtifactSplitter.test.ts +0 -38
- package/src/chunking/ArtifactSplitter.ts +0 -151
- package/src/debug/AGENTS.md +0 -79
- package/src/debug/logger.test.ts +0 -244
- package/src/debug/logger.ts +0 -211
- package/src/extract.test.ts +0 -22
- package/src/extract.ts +0 -150
- package/src/fields.test.ts +0 -681
- package/src/fields.ts +0 -246
- package/src/index.test.ts +0 -20
- package/src/index.ts +0 -110
- package/src/llm/AGENTS.md +0 -9
- package/src/llm/LLMClient.test.ts +0 -394
- package/src/llm/LLMClient.ts +0 -264
- package/src/llm/RetryingRunner.test.ts +0 -174
- package/src/llm/RetryingRunner.ts +0 -270
- package/src/llm/message.test.ts +0 -42
- package/src/llm/message.ts +0 -47
- package/src/llm/models.test.ts +0 -82
- package/src/llm/models.ts +0 -190
- package/src/llm/resolveModel.ts +0 -86
- package/src/merge/AGENTS.md +0 -6
- package/src/merge/Deduplicator.test.ts +0 -108
- package/src/merge/Deduplicator.ts +0 -45
- package/src/merge/SmartDataMerger.test.ts +0 -177
- package/src/merge/SmartDataMerger.ts +0 -56
- package/src/parsers/AGENTS.md +0 -58
- package/src/parsers/collect.test.ts +0 -56
- package/src/parsers/collect.ts +0 -31
- package/src/parsers/index.ts +0 -6
- package/src/parsers/mime.test.ts +0 -91
- package/src/parsers/mime.ts +0 -137
- package/src/parsers/npm.ts +0 -26
- package/src/parsers/pdf.test.ts +0 -394
- package/src/parsers/pdf.ts +0 -194
- package/src/parsers/runner.test.ts +0 -95
- package/src/parsers/runner.ts +0 -177
- package/src/parsers/types.ts +0 -29
- package/src/prompts/AGENTS.md +0 -8
- package/src/prompts/DeduplicationPrompt.test.ts +0 -41
- package/src/prompts/DeduplicationPrompt.ts +0 -37
- package/src/prompts/ExtractorPrompt.test.ts +0 -21
- package/src/prompts/ExtractorPrompt.ts +0 -72
- package/src/prompts/ParallelMergerPrompt.test.ts +0 -8
- package/src/prompts/ParallelMergerPrompt.ts +0 -37
- package/src/prompts/SequentialExtractorPrompt.test.ts +0 -24
- package/src/prompts/SequentialExtractorPrompt.ts +0 -82
- package/src/prompts/formatArtifacts.test.ts +0 -39
- package/src/prompts/formatArtifacts.ts +0 -46
- package/src/strategies/AGENTS.md +0 -6
- package/src/strategies/DoublePassAutoMergeStrategy.test.ts +0 -53
- package/src/strategies/DoublePassAutoMergeStrategy.ts +0 -410
- package/src/strategies/DoublePassStrategy.test.ts +0 -48
- package/src/strategies/DoublePassStrategy.ts +0 -266
- package/src/strategies/ParallelAutoMergeStrategy.test.ts +0 -152
- package/src/strategies/ParallelAutoMergeStrategy.ts +0 -345
- package/src/strategies/ParallelStrategy.test.ts +0 -61
- package/src/strategies/ParallelStrategy.ts +0 -208
- package/src/strategies/SequentialAutoMergeStrategy.test.ts +0 -66
- package/src/strategies/SequentialAutoMergeStrategy.ts +0 -325
- package/src/strategies/SequentialStrategy.test.ts +0 -53
- package/src/strategies/SequentialStrategy.ts +0 -142
- package/src/strategies/SimpleStrategy.test.ts +0 -46
- package/src/strategies/SimpleStrategy.ts +0 -94
- package/src/strategies/concurrency.test.ts +0 -16
- package/src/strategies/concurrency.ts +0 -14
- package/src/strategies/index.test.ts +0 -20
- package/src/strategies/index.ts +0 -7
- package/src/strategies/utils.test.ts +0 -76
- package/src/strategies/utils.ts +0 -95
- package/src/tokenization.test.ts +0 -119
- package/src/tokenization.ts +0 -71
- package/src/types.test.ts +0 -25
- package/src/types.ts +0 -174
- package/src/validation/AGENTS.md +0 -7
- package/src/validation/validator.test.ts +0 -204
- package/src/validation/validator.ts +0 -90
- package/tsconfig.json +0 -22
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
import { test, expect, mock } from "bun:test";
|
|
2
|
-
import type { ModelMessage } from "ai";
|
|
3
|
-
|
|
4
|
-
type GenerateTextParams = {
|
|
5
|
-
model: unknown;
|
|
6
|
-
output: unknown;
|
|
7
|
-
system: string;
|
|
8
|
-
messages: ModelMessage[];
|
|
9
|
-
providerOptions?: unknown;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
let generateTextImpl: (params: GenerateTextParams) => Promise<{
|
|
13
|
-
output: unknown;
|
|
14
|
-
usage?: Record<string, unknown>;
|
|
15
|
-
}>;
|
|
16
|
-
|
|
17
|
-
const calls: GenerateTextParams[] = [];
|
|
18
|
-
|
|
19
|
-
mock.module("ai", () => ({
|
|
20
|
-
generateText: (params: GenerateTextParams) => {
|
|
21
|
-
calls.push(params);
|
|
22
|
-
return generateTextImpl(params);
|
|
23
|
-
},
|
|
24
|
-
Output: {
|
|
25
|
-
object: (config: unknown) => config,
|
|
26
|
-
},
|
|
27
|
-
jsonSchema: (schema: unknown) => ({ wrapped: schema }),
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
const { generateStructured } = await import("./LLMClient");
|
|
31
|
-
|
|
32
|
-
test("generateStructured maps prompt/completion token usage", async () => {
|
|
33
|
-
calls.length = 0;
|
|
34
|
-
generateTextImpl = async () => ({
|
|
35
|
-
output: { title: "ok" },
|
|
36
|
-
usage: { promptTokens: 2, completionTokens: 3, totalTokens: 9 },
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const result = await generateStructured({
|
|
40
|
-
model: {},
|
|
41
|
-
schema: { type: "object" },
|
|
42
|
-
system: "sys",
|
|
43
|
-
user: "prompt",
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
expect(result.usage).toEqual({ inputTokens: 2, outputTokens: 3, totalTokens: 9 });
|
|
47
|
-
expect(calls[0]?.output).toEqual({ schema: { wrapped: { type: "object" } }, name: "extract" });
|
|
48
|
-
expect(calls[0]?.messages[0]).toEqual({ role: "user", content: "prompt" });
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("generateStructured uses explicit messages and totals usage", async () => {
|
|
52
|
-
calls.length = 0;
|
|
53
|
-
const messages: ModelMessage[] = [{ role: "user", content: "custom" }];
|
|
54
|
-
generateTextImpl = async (params) => ({
|
|
55
|
-
output: { title: "ok" },
|
|
56
|
-
usage: { inputTokens: 4, outputTokens: 6 },
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const result = await generateStructured({
|
|
60
|
-
model: {},
|
|
61
|
-
schema: { type: "object" },
|
|
62
|
-
system: "sys",
|
|
63
|
-
user: "fallback",
|
|
64
|
-
messages,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
expect(calls[0]?.messages).toBe(messages);
|
|
68
|
-
expect(result.usage).toEqual({ inputTokens: 4, outputTokens: 6, totalTokens: 10 });
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("generateStructured passes OpenRouter provider preference", async () => {
|
|
72
|
-
calls.length = 0;
|
|
73
|
-
generateTextImpl = async () => ({
|
|
74
|
-
output: { title: "ok" },
|
|
75
|
-
usage: { inputTokens: 1, outputTokens: 1 },
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const model = { __openrouter_provider: "cerebras" };
|
|
79
|
-
await generateStructured({
|
|
80
|
-
model,
|
|
81
|
-
schema: { type: "object" },
|
|
82
|
-
system: "sys",
|
|
83
|
-
user: "prompt",
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
expect(calls[0]?.providerOptions).toEqual({
|
|
87
|
-
openrouter: {
|
|
88
|
-
provider: {
|
|
89
|
-
order: ["cerebras"],
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("generateStructured does not add openrouter providerOptions without preference", async () => {
|
|
96
|
-
calls.length = 0;
|
|
97
|
-
generateTextImpl = async () => ({
|
|
98
|
-
output: { title: "ok" },
|
|
99
|
-
usage: { inputTokens: 1, outputTokens: 1 },
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
await generateStructured({
|
|
103
|
-
model: {},
|
|
104
|
-
schema: { type: "object" },
|
|
105
|
-
system: "sys",
|
|
106
|
-
user: "prompt",
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
expect(calls[0]?.providerOptions).not.toHaveProperty("openrouter");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("generateStructured uses inputTokens/outputTokens when promptTokens missing", async () => {
|
|
113
|
-
calls.length = 0;
|
|
114
|
-
generateTextImpl = async () => ({
|
|
115
|
-
output: { title: "ok" },
|
|
116
|
-
usage: { inputTokens: 5, outputTokens: 7 },
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const result = await generateStructured({
|
|
120
|
-
model: {},
|
|
121
|
-
schema: { type: "object" },
|
|
122
|
-
system: "sys",
|
|
123
|
-
user: "prompt",
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
expect(result.usage).toEqual({ inputTokens: 5, outputTokens: 7, totalTokens: 12 });
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("generateStructured uses totalTokens from response when present", async () => {
|
|
130
|
-
calls.length = 0;
|
|
131
|
-
generateTextImpl = async () => ({
|
|
132
|
-
output: { title: "ok" },
|
|
133
|
-
usage: { inputTokens: 3, outputTokens: 4, totalTokens: 100 },
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const result = await generateStructured({
|
|
137
|
-
model: {},
|
|
138
|
-
schema: { type: "object" },
|
|
139
|
-
system: "sys",
|
|
140
|
-
user: "prompt",
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
expect(result.usage.totalTokens).toBe(100);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("generateStructured handles missing usage", async () => {
|
|
147
|
-
calls.length = 0;
|
|
148
|
-
generateTextImpl = async () => ({
|
|
149
|
-
output: { title: "ok" },
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
const result = await generateStructured({
|
|
153
|
-
model: {},
|
|
154
|
-
schema: { type: "object" },
|
|
155
|
-
system: "sys",
|
|
156
|
-
user: "prompt",
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
expect(result.usage).toEqual({ inputTokens: 0, outputTokens: 0, totalTokens: 0 });
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("generateStructured uses custom schema name", async () => {
|
|
163
|
-
calls.length = 0;
|
|
164
|
-
generateTextImpl = async () => ({
|
|
165
|
-
output: { title: "ok" },
|
|
166
|
-
usage: {},
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
await generateStructured({
|
|
170
|
-
model: {},
|
|
171
|
-
schema: { type: "object" },
|
|
172
|
-
schemaName: "custom_schema",
|
|
173
|
-
system: "sys",
|
|
174
|
-
user: "prompt",
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
expect(calls[0]?.output).toHaveProperty("name", "custom_schema");
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("generateStructured uses custom schema description", async () => {
|
|
181
|
-
calls.length = 0;
|
|
182
|
-
generateTextImpl = async () => ({
|
|
183
|
-
output: { title: "ok" },
|
|
184
|
-
usage: {},
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
await generateStructured({
|
|
188
|
-
model: {},
|
|
189
|
-
schema: { type: "object" },
|
|
190
|
-
schemaDescription: "Extract data",
|
|
191
|
-
system: "sys",
|
|
192
|
-
user: "prompt",
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
expect(calls[0]?.output).toHaveProperty("description", "Extract data");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("generateStructured shows friendly error when model doesn't support images", async () => {
|
|
199
|
-
calls.length = 0;
|
|
200
|
-
generateTextImpl = async () => {
|
|
201
|
-
throw {
|
|
202
|
-
responseBody: '{"error":{"message":"No endpoints found that support image input","code":404}}',
|
|
203
|
-
statusCode: 404,
|
|
204
|
-
};
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
expect(
|
|
208
|
-
async () =>
|
|
209
|
-
await generateStructured({
|
|
210
|
-
model: { modelId: "meta-llama/llama-3.1-8b-instruct" },
|
|
211
|
-
schema: { type: "object" },
|
|
212
|
-
system: "sys",
|
|
213
|
-
user: [{ type: "text", text: "prompt" }, { type: "image", image: "base64data" }],
|
|
214
|
-
}),
|
|
215
|
-
).toThrow(
|
|
216
|
-
'Model "meta-llama/llama-3.1-8b-instruct" does not support image input. Please use a model that supports images (e.g., gpt-4o, claude-3-5-sonnet, gemini-1.5-pro) or remove the --images and --screenshots flags.',
|
|
217
|
-
);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test("generateStructured rethrows other API errors", async () => {
|
|
221
|
-
calls.length = 0;
|
|
222
|
-
const originalError = new Error("Some other error");
|
|
223
|
-
generateTextImpl = async () => {
|
|
224
|
-
throw originalError;
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
expect(
|
|
228
|
-
async () =>
|
|
229
|
-
await generateStructured({
|
|
230
|
-
model: {},
|
|
231
|
-
schema: { type: "object" },
|
|
232
|
-
system: "sys",
|
|
233
|
-
user: "prompt",
|
|
234
|
-
}),
|
|
235
|
-
).toThrow("Some other error");
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test("generateStructured shows friendly error for internal server error", async () => {
|
|
239
|
-
calls.length = 0;
|
|
240
|
-
generateTextImpl = async () => {
|
|
241
|
-
throw {
|
|
242
|
-
statusCode: 200,
|
|
243
|
-
responseBody: undefined,
|
|
244
|
-
data: {
|
|
245
|
-
code: 500,
|
|
246
|
-
message: "Internal Server Error",
|
|
247
|
-
type: null,
|
|
248
|
-
param: null,
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
expect(
|
|
254
|
-
async () =>
|
|
255
|
-
await generateStructured({
|
|
256
|
-
model: { modelId: "openai/gpt-5-mini" },
|
|
257
|
-
schema: { type: "object" },
|
|
258
|
-
system: "sys",
|
|
259
|
-
user: "prompt",
|
|
260
|
-
}),
|
|
261
|
-
).toThrow(
|
|
262
|
-
'Provider error for model "openai/gpt-5-mini": Internal server error. The model or provider may be experiencing issues. Please try again or use a different model.',
|
|
263
|
-
);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test("generateStructured shows friendly error for authentication failure", async () => {
|
|
267
|
-
calls.length = 0;
|
|
268
|
-
generateTextImpl = async () => {
|
|
269
|
-
throw {
|
|
270
|
-
statusCode: 401,
|
|
271
|
-
responseBody: '{"error":{"message":"Invalid API key"}}',
|
|
272
|
-
data: {
|
|
273
|
-
code: 401,
|
|
274
|
-
message: "Invalid API key",
|
|
275
|
-
},
|
|
276
|
-
};
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
expect(
|
|
280
|
-
async () =>
|
|
281
|
-
await generateStructured({
|
|
282
|
-
model: { modelId: "gpt-4o" },
|
|
283
|
-
schema: { type: "object" },
|
|
284
|
-
system: "sys",
|
|
285
|
-
user: "prompt",
|
|
286
|
-
}),
|
|
287
|
-
).toThrow(
|
|
288
|
-
'Authentication failed for model "gpt-4o". Please check your API key is valid and has the necessary permissions.',
|
|
289
|
-
);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
test("generateStructured shows friendly error for rate limit", async () => {
|
|
293
|
-
calls.length = 0;
|
|
294
|
-
generateTextImpl = async () => {
|
|
295
|
-
throw {
|
|
296
|
-
statusCode: 429,
|
|
297
|
-
responseBody: '{"error":{"message":"Rate limit exceeded"}}',
|
|
298
|
-
data: {
|
|
299
|
-
code: 429,
|
|
300
|
-
message: "Rate limit exceeded",
|
|
301
|
-
},
|
|
302
|
-
};
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
expect(
|
|
306
|
-
async () =>
|
|
307
|
-
await generateStructured({
|
|
308
|
-
model: { modelId: "claude-3-5-sonnet" },
|
|
309
|
-
schema: { type: "object" },
|
|
310
|
-
system: "sys",
|
|
311
|
-
user: "prompt",
|
|
312
|
-
}),
|
|
313
|
-
).toThrow(
|
|
314
|
-
'Rate limit exceeded for model "claude-3-5-sonnet". Please wait a moment and try again, or use a different model.',
|
|
315
|
-
);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test("generateStructured shows friendly error for model not found", async () => {
|
|
319
|
-
calls.length = 0;
|
|
320
|
-
generateTextImpl = async () => {
|
|
321
|
-
throw {
|
|
322
|
-
statusCode: 404,
|
|
323
|
-
responseBody: '{"error":{"message":"Model not found"}}',
|
|
324
|
-
data: {
|
|
325
|
-
code: 404,
|
|
326
|
-
message: "Model not found",
|
|
327
|
-
},
|
|
328
|
-
};
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
expect(
|
|
332
|
-
async () =>
|
|
333
|
-
await generateStructured({
|
|
334
|
-
model: { modelId: "nonexistent-model" },
|
|
335
|
-
schema: { type: "object" },
|
|
336
|
-
system: "sys",
|
|
337
|
-
user: "prompt",
|
|
338
|
-
}),
|
|
339
|
-
).toThrow(
|
|
340
|
-
'Model "nonexistent-model" not found or unavailable. Model not found Please check the model name or try a different model.',
|
|
341
|
-
);
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
test("generateStructured shows friendly error for access denied", async () => {
|
|
345
|
-
calls.length = 0;
|
|
346
|
-
generateTextImpl = async () => {
|
|
347
|
-
throw {
|
|
348
|
-
statusCode: 403,
|
|
349
|
-
responseBody: '{"error":{"message":"Access denied"}}',
|
|
350
|
-
data: {
|
|
351
|
-
code: 403,
|
|
352
|
-
message: "Access denied",
|
|
353
|
-
},
|
|
354
|
-
};
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
expect(
|
|
358
|
-
async () =>
|
|
359
|
-
await generateStructured({
|
|
360
|
-
model: { modelId: "gpt-4-turbo" },
|
|
361
|
-
schema: { type: "object" },
|
|
362
|
-
system: "sys",
|
|
363
|
-
user: "prompt",
|
|
364
|
-
}),
|
|
365
|
-
).toThrow(
|
|
366
|
-
'Access denied for model "gpt-4-turbo". Your API key may not have access to this model. Please check your subscription or try a different model.',
|
|
367
|
-
);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
test("generateStructured shows generic provider error message", async () => {
|
|
371
|
-
calls.length = 0;
|
|
372
|
-
generateTextImpl = async () => {
|
|
373
|
-
throw {
|
|
374
|
-
statusCode: 400,
|
|
375
|
-
responseBody: '{"error":{"message":"Context length exceeded"}}',
|
|
376
|
-
data: {
|
|
377
|
-
code: 400,
|
|
378
|
-
message: "Context length exceeded",
|
|
379
|
-
},
|
|
380
|
-
};
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
expect(
|
|
384
|
-
async () =>
|
|
385
|
-
await generateStructured({
|
|
386
|
-
model: { modelId: "gpt-3.5-turbo" },
|
|
387
|
-
schema: { type: "object" },
|
|
388
|
-
system: "sys",
|
|
389
|
-
user: "prompt",
|
|
390
|
-
}),
|
|
391
|
-
).toThrow(
|
|
392
|
-
'Provider error for model "gpt-3.5-turbo": Context length exceeded',
|
|
393
|
-
);
|
|
394
|
-
});
|
package/src/llm/LLMClient.ts
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import { generateText, Output, jsonSchema, type ModelMessage } from "ai";
|
|
2
|
-
import type { AnyJSONSchema, Usage, TelemetryAdapter } from "../types";
|
|
3
|
-
import type { UserContent } from "./message";
|
|
4
|
-
|
|
5
|
-
type GenerateTextParams = Parameters<typeof generateText>[0];
|
|
6
|
-
type ModelType = GenerateTextParams extends { model: infer M } ? M : unknown;
|
|
7
|
-
type MessageType = Array<ModelMessage>;
|
|
8
|
-
|
|
9
|
-
export type StructuredRequest<T> = {
|
|
10
|
-
model: ModelType | unknown;
|
|
11
|
-
system: string;
|
|
12
|
-
user: UserContent;
|
|
13
|
-
messages?: MessageType;
|
|
14
|
-
schema: unknown;
|
|
15
|
-
schemaName?: string;
|
|
16
|
-
schemaDescription?: string;
|
|
17
|
-
strict?: boolean;
|
|
18
|
-
/**
|
|
19
|
-
* Telemetry adapter for tracing LLM calls
|
|
20
|
-
*/
|
|
21
|
-
telemetry?: TelemetryAdapter;
|
|
22
|
-
/**
|
|
23
|
-
* Parent span for creating hierarchical traces
|
|
24
|
-
*/
|
|
25
|
-
parentSpan?: { id: string; traceId: string; name: string; kind: string; startTime: number; parentId?: string };
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type StructuredResponse<T> = {
|
|
29
|
-
data: T;
|
|
30
|
-
usage: Usage;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const isZodSchema = (
|
|
34
|
-
schema: unknown,
|
|
35
|
-
): schema is { safeParse: (data: unknown) => unknown } => {
|
|
36
|
-
return (
|
|
37
|
-
typeof schema === "object" &&
|
|
38
|
-
schema !== null &&
|
|
39
|
-
"safeParse" in schema &&
|
|
40
|
-
typeof (schema as { safeParse?: unknown }).safeParse === "function"
|
|
41
|
-
);
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export const generateStructured = async <T>(
|
|
45
|
-
request: StructuredRequest<T>,
|
|
46
|
-
): Promise<StructuredResponse<T>> => {
|
|
47
|
-
const { telemetry, parentSpan } = request;
|
|
48
|
-
|
|
49
|
-
// Start LLM span if telemetry is enabled
|
|
50
|
-
const llmSpan = telemetry?.startSpan({
|
|
51
|
-
name: "llm.generateStructured",
|
|
52
|
-
kind: "LLM",
|
|
53
|
-
parentSpan,
|
|
54
|
-
attributes: {
|
|
55
|
-
"llm.schema_name": request.schemaName ?? "extract",
|
|
56
|
-
"llm.strict": request.strict ?? false,
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const startTime = Date.now();
|
|
61
|
-
|
|
62
|
-
const schema = isZodSchema(request.schema)
|
|
63
|
-
? request.schema
|
|
64
|
-
: jsonSchema(request.schema as AnyJSONSchema);
|
|
65
|
-
|
|
66
|
-
// Check for OpenRouter provider preference attached to the model
|
|
67
|
-
const preferredProvider = (
|
|
68
|
-
request.model as { __openrouter_provider?: string }
|
|
69
|
-
)?.__openrouter_provider;
|
|
70
|
-
|
|
71
|
-
if (preferredProvider && process.env.DEBUG) {
|
|
72
|
-
console.error(
|
|
73
|
-
`[DEBUG] Routing to OpenRouter provider: ${preferredProvider}`,
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const providerOptions = preferredProvider
|
|
78
|
-
? {
|
|
79
|
-
openrouter: {
|
|
80
|
-
provider: {
|
|
81
|
-
order: [preferredProvider],
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
}
|
|
85
|
-
: undefined;
|
|
86
|
-
|
|
87
|
-
let result;
|
|
88
|
-
try {
|
|
89
|
-
result = await generateText({
|
|
90
|
-
model: request.model as ModelType,
|
|
91
|
-
output: Output.object({
|
|
92
|
-
schema: schema as GenerateTextParams extends { schema: infer S }
|
|
93
|
-
? S
|
|
94
|
-
: never,
|
|
95
|
-
name: request.schemaName ?? "extract",
|
|
96
|
-
description: request.schemaDescription,
|
|
97
|
-
}),
|
|
98
|
-
providerOptions: {
|
|
99
|
-
openai: {
|
|
100
|
-
strictJsonSchema: request.strict ?? false,
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
system: request.system,
|
|
104
|
-
messages: (request.messages ?? [
|
|
105
|
-
{ role: "user", content: request.user },
|
|
106
|
-
]) as MessageType,
|
|
107
|
-
...(providerOptions ? { providerOptions } : {}),
|
|
108
|
-
});
|
|
109
|
-
} catch (error) {
|
|
110
|
-
// Determine model ID for error messages
|
|
111
|
-
const modelId =
|
|
112
|
-
typeof request.model === "object" && request.model !== null
|
|
113
|
-
? (request.model as { modelId?: string }).modelId ??
|
|
114
|
-
JSON.stringify(request.model)
|
|
115
|
-
: String(request.model);
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
error &&
|
|
119
|
-
typeof error === "object" &&
|
|
120
|
-
"responseBody" in error &&
|
|
121
|
-
"statusCode" in error
|
|
122
|
-
) {
|
|
123
|
-
const apiError = error as {
|
|
124
|
-
responseBody: unknown;
|
|
125
|
-
statusCode: number;
|
|
126
|
-
data?: {
|
|
127
|
-
code?: number;
|
|
128
|
-
message?: string;
|
|
129
|
-
type?: string | null;
|
|
130
|
-
param?: string | null;
|
|
131
|
-
};
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const responseBody = apiError.responseBody;
|
|
135
|
-
const errorData = apiError.data;
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
typeof responseBody === "string" &&
|
|
139
|
-
responseBody.includes("No endpoints found that support image input")
|
|
140
|
-
) {
|
|
141
|
-
throw new Error(
|
|
142
|
-
`Model "${modelId}" does not support image input. Please use a model that supports images (e.g., gpt-4o, claude-3-5-sonnet, gemini-1.5-pro) or remove the --images and --screenshots flags.`,
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (errorData?.code === 500 || errorData?.message?.includes("Internal Server Error")) {
|
|
147
|
-
throw new Error(
|
|
148
|
-
`Provider error for model "${modelId}": Internal server error. The model or provider may be experiencing issues. Please try again or use a different model.`,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (apiError.statusCode === 401 || errorData?.code === 401) {
|
|
153
|
-
throw new Error(
|
|
154
|
-
`Authentication failed for model "${modelId}". Please check your API key is valid and has the necessary permissions.`,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (apiError.statusCode === 403 || errorData?.code === 403) {
|
|
159
|
-
throw new Error(
|
|
160
|
-
`Access denied for model "${modelId}". Your API key may not have access to this model. Please check your subscription or try a different model.`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (apiError.statusCode === 429 || errorData?.code === 429) {
|
|
165
|
-
throw new Error(
|
|
166
|
-
`Rate limit exceeded for model "${modelId}". Please wait a moment and try again, or use a different model.`,
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (apiError.statusCode === 404 || errorData?.code === 404) {
|
|
171
|
-
const errorMsg = errorData?.message || "Model not found";
|
|
172
|
-
throw new Error(
|
|
173
|
-
`Model "${modelId}" not found or unavailable. ${errorMsg} Please check the model name or try a different model.`,
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (errorData?.message) {
|
|
178
|
-
throw new Error(
|
|
179
|
-
`Provider error for model "${modelId}": ${errorData.message}`,
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Record error in telemetry
|
|
185
|
-
if (llmSpan && telemetry) {
|
|
186
|
-
const latencyMs = Date.now() - startTime;
|
|
187
|
-
telemetry.recordEvent(llmSpan, {
|
|
188
|
-
type: "llm_call",
|
|
189
|
-
model: modelId,
|
|
190
|
-
provider: "unknown", // Will be determined by the model
|
|
191
|
-
input: {
|
|
192
|
-
messages: request.messages ?? [{ role: "user", content: typeof request.user === "string" ? request.user : "" }],
|
|
193
|
-
temperature: undefined,
|
|
194
|
-
maxTokens: undefined,
|
|
195
|
-
schema: request.schema,
|
|
196
|
-
},
|
|
197
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
198
|
-
latencyMs,
|
|
199
|
-
});
|
|
200
|
-
telemetry.endSpan(llmSpan, {
|
|
201
|
-
status: "error",
|
|
202
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
203
|
-
latencyMs,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
throw error;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const usageRaw = result.usage ?? {};
|
|
211
|
-
const inputTokens =
|
|
212
|
-
"promptTokens" in usageRaw
|
|
213
|
-
? (usageRaw.promptTokens as number)
|
|
214
|
-
: ((usageRaw as { inputTokens?: number }).inputTokens ?? 0);
|
|
215
|
-
const outputTokens =
|
|
216
|
-
"completionTokens" in usageRaw
|
|
217
|
-
? (usageRaw.completionTokens as number)
|
|
218
|
-
: ((usageRaw as { outputTokens?: number }).outputTokens ?? 0);
|
|
219
|
-
const totalTokens =
|
|
220
|
-
"totalTokens" in usageRaw
|
|
221
|
-
? (usageRaw.totalTokens as number)
|
|
222
|
-
: inputTokens + outputTokens;
|
|
223
|
-
|
|
224
|
-
const usage: Usage = {
|
|
225
|
-
inputTokens,
|
|
226
|
-
outputTokens,
|
|
227
|
-
totalTokens,
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
// Record successful LLM call in telemetry
|
|
231
|
-
if (llmSpan && telemetry) {
|
|
232
|
-
const latencyMs = Date.now() - startTime;
|
|
233
|
-
telemetry.recordEvent(llmSpan, {
|
|
234
|
-
type: "llm_call",
|
|
235
|
-
model: typeof request.model === "object" && request.model !== null
|
|
236
|
-
? (request.model as { modelId?: string }).modelId ?? "unknown"
|
|
237
|
-
: String(request.model),
|
|
238
|
-
provider: preferredProvider ?? "unknown",
|
|
239
|
-
input: {
|
|
240
|
-
messages: request.messages ?? [{ role: "user", content: typeof request.user === "string" ? request.user : "" }],
|
|
241
|
-
temperature: undefined,
|
|
242
|
-
maxTokens: undefined,
|
|
243
|
-
schema: request.schema,
|
|
244
|
-
},
|
|
245
|
-
output: {
|
|
246
|
-
content: JSON.stringify(result.output),
|
|
247
|
-
structured: true,
|
|
248
|
-
usage: {
|
|
249
|
-
input: inputTokens,
|
|
250
|
-
output: outputTokens,
|
|
251
|
-
total: totalTokens,
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
latencyMs,
|
|
255
|
-
});
|
|
256
|
-
telemetry.endSpan(llmSpan, {
|
|
257
|
-
status: "ok",
|
|
258
|
-
output: result.output,
|
|
259
|
-
latencyMs,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return { data: result.output as T, usage };
|
|
264
|
-
};
|