@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.
Files changed (105) hide show
  1. package/dist/index.js +4111 -0
  2. package/dist/index.js.map +1 -0
  3. package/dist/parsers.js +492 -0
  4. package/dist/parsers.js.map +1 -0
  5. package/dist/strategies.js +2435 -0
  6. package/dist/strategies.js.map +1 -0
  7. package/package.json +25 -13
  8. package/src/agent-cli-integration.test.ts +0 -47
  9. package/src/agent-export.test.ts +0 -17
  10. package/src/agent-tool-labels.test.ts +0 -50
  11. package/src/artifacts/AGENTS.md +0 -16
  12. package/src/artifacts/fileToArtifact.test.ts +0 -37
  13. package/src/artifacts/fileToArtifact.ts +0 -44
  14. package/src/artifacts/input.test.ts +0 -243
  15. package/src/artifacts/input.ts +0 -360
  16. package/src/artifacts/providers.test.ts +0 -19
  17. package/src/artifacts/providers.ts +0 -7
  18. package/src/artifacts/urlToArtifact.test.ts +0 -23
  19. package/src/artifacts/urlToArtifact.ts +0 -19
  20. package/src/auth/AGENTS.md +0 -11
  21. package/src/auth/config.test.ts +0 -132
  22. package/src/auth/config.ts +0 -186
  23. package/src/auth/tokens.test.ts +0 -58
  24. package/src/auth/tokens.ts +0 -229
  25. package/src/chunking/AGENTS.md +0 -11
  26. package/src/chunking/ArtifactBatcher.test.ts +0 -22
  27. package/src/chunking/ArtifactBatcher.ts +0 -110
  28. package/src/chunking/ArtifactSplitter.test.ts +0 -38
  29. package/src/chunking/ArtifactSplitter.ts +0 -151
  30. package/src/debug/AGENTS.md +0 -79
  31. package/src/debug/logger.test.ts +0 -244
  32. package/src/debug/logger.ts +0 -211
  33. package/src/extract.test.ts +0 -22
  34. package/src/extract.ts +0 -150
  35. package/src/fields.test.ts +0 -681
  36. package/src/fields.ts +0 -246
  37. package/src/index.test.ts +0 -20
  38. package/src/index.ts +0 -110
  39. package/src/llm/AGENTS.md +0 -9
  40. package/src/llm/LLMClient.test.ts +0 -394
  41. package/src/llm/LLMClient.ts +0 -264
  42. package/src/llm/RetryingRunner.test.ts +0 -174
  43. package/src/llm/RetryingRunner.ts +0 -270
  44. package/src/llm/message.test.ts +0 -42
  45. package/src/llm/message.ts +0 -47
  46. package/src/llm/models.test.ts +0 -82
  47. package/src/llm/models.ts +0 -190
  48. package/src/llm/resolveModel.ts +0 -86
  49. package/src/merge/AGENTS.md +0 -6
  50. package/src/merge/Deduplicator.test.ts +0 -108
  51. package/src/merge/Deduplicator.ts +0 -45
  52. package/src/merge/SmartDataMerger.test.ts +0 -177
  53. package/src/merge/SmartDataMerger.ts +0 -56
  54. package/src/parsers/AGENTS.md +0 -58
  55. package/src/parsers/collect.test.ts +0 -56
  56. package/src/parsers/collect.ts +0 -31
  57. package/src/parsers/index.ts +0 -6
  58. package/src/parsers/mime.test.ts +0 -91
  59. package/src/parsers/mime.ts +0 -137
  60. package/src/parsers/npm.ts +0 -26
  61. package/src/parsers/pdf.test.ts +0 -394
  62. package/src/parsers/pdf.ts +0 -194
  63. package/src/parsers/runner.test.ts +0 -95
  64. package/src/parsers/runner.ts +0 -177
  65. package/src/parsers/types.ts +0 -29
  66. package/src/prompts/AGENTS.md +0 -8
  67. package/src/prompts/DeduplicationPrompt.test.ts +0 -41
  68. package/src/prompts/DeduplicationPrompt.ts +0 -37
  69. package/src/prompts/ExtractorPrompt.test.ts +0 -21
  70. package/src/prompts/ExtractorPrompt.ts +0 -72
  71. package/src/prompts/ParallelMergerPrompt.test.ts +0 -8
  72. package/src/prompts/ParallelMergerPrompt.ts +0 -37
  73. package/src/prompts/SequentialExtractorPrompt.test.ts +0 -24
  74. package/src/prompts/SequentialExtractorPrompt.ts +0 -82
  75. package/src/prompts/formatArtifacts.test.ts +0 -39
  76. package/src/prompts/formatArtifacts.ts +0 -46
  77. package/src/strategies/AGENTS.md +0 -6
  78. package/src/strategies/DoublePassAutoMergeStrategy.test.ts +0 -53
  79. package/src/strategies/DoublePassAutoMergeStrategy.ts +0 -410
  80. package/src/strategies/DoublePassStrategy.test.ts +0 -48
  81. package/src/strategies/DoublePassStrategy.ts +0 -266
  82. package/src/strategies/ParallelAutoMergeStrategy.test.ts +0 -152
  83. package/src/strategies/ParallelAutoMergeStrategy.ts +0 -345
  84. package/src/strategies/ParallelStrategy.test.ts +0 -61
  85. package/src/strategies/ParallelStrategy.ts +0 -208
  86. package/src/strategies/SequentialAutoMergeStrategy.test.ts +0 -66
  87. package/src/strategies/SequentialAutoMergeStrategy.ts +0 -325
  88. package/src/strategies/SequentialStrategy.test.ts +0 -53
  89. package/src/strategies/SequentialStrategy.ts +0 -142
  90. package/src/strategies/SimpleStrategy.test.ts +0 -46
  91. package/src/strategies/SimpleStrategy.ts +0 -94
  92. package/src/strategies/concurrency.test.ts +0 -16
  93. package/src/strategies/concurrency.ts +0 -14
  94. package/src/strategies/index.test.ts +0 -20
  95. package/src/strategies/index.ts +0 -7
  96. package/src/strategies/utils.test.ts +0 -76
  97. package/src/strategies/utils.ts +0 -95
  98. package/src/tokenization.test.ts +0 -119
  99. package/src/tokenization.ts +0 -71
  100. package/src/types.test.ts +0 -25
  101. package/src/types.ts +0 -174
  102. package/src/validation/AGENTS.md +0 -7
  103. package/src/validation/validator.test.ts +0 -204
  104. package/src/validation/validator.ts +0 -90
  105. package/tsconfig.json +0 -22
@@ -1,174 +0,0 @@
1
- import { test, expect } from "bun:test";
2
- import type { JSONSchemaType } from "ajv";
3
- import { runWithRetries } from "./RetryingRunner";
4
-
5
- type Output = { title: string };
6
-
7
- const schema: JSONSchemaType<Output> = {
8
- type: "object",
9
- properties: { title: { type: "string" } },
10
- required: ["title"],
11
- additionalProperties: false,
12
- };
13
-
14
- test("runWithRetries emits onRetry event when retrying", async () => {
15
- let calls = 0;
16
- const retryEvents: Array<{ attempt: number; maxAttempts: number; reason?: string }> = [];
17
-
18
- const result = await runWithRetries<Output>({
19
- model: {},
20
- schema,
21
- system: "sys",
22
- user: "user",
23
- execute: async () => {
24
- calls += 1;
25
- if (calls === 1) {
26
- return {
27
- data: { title: 123 } as unknown as Output,
28
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
29
- };
30
- }
31
- return {
32
- data: { title: "ok" },
33
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
34
- };
35
- },
36
- events: {
37
- onRetry: (info) => {
38
- retryEvents.push(info);
39
- },
40
- },
41
- });
42
-
43
- expect(result.data.title).toBe("ok");
44
- expect(calls).toBe(2);
45
- expect(retryEvents).toHaveLength(1);
46
- expect(retryEvents[0]?.attempt).toBe(2);
47
- expect(retryEvents[0]?.maxAttempts).toBe(3);
48
- expect(retryEvents[0]?.reason).toBe("schema_validation_failed");
49
- });
50
-
51
- test("runWithRetries retries on validation error", async () => {
52
- let calls = 0;
53
- const result = await runWithRetries<Output>({
54
- model: {},
55
- schema,
56
- system: "sys",
57
- user: "user",
58
- execute: async () => {
59
- calls += 1;
60
- if (calls === 1) {
61
- return {
62
- data: { title: 123 } as unknown as Output,
63
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
64
- };
65
- }
66
- return {
67
- data: { title: "ok" },
68
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
69
- };
70
- },
71
- });
72
-
73
- expect(result.data.title).toBe("ok");
74
- expect(calls).toBe(2);
75
- });
76
-
77
- test("runWithRetries with strict=false retries on missing required fields until max attempts", async () => {
78
- let calls = 0;
79
-
80
- await expect(
81
- runWithRetries<Output>({
82
- model: {},
83
- schema,
84
- system: "sys",
85
- user: "user",
86
- strict: false,
87
- maxAttempts: 2,
88
- execute: async () => {
89
- calls += 1;
90
- return {
91
- data: {} as Output,
92
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
93
- };
94
- },
95
- })
96
- ).rejects.toThrow();
97
-
98
- expect(calls).toBe(2);
99
- });
100
-
101
- test("runWithRetries with strict=true validates required fields on every attempt", async () => {
102
- let calls = 0;
103
-
104
- await expect(
105
- runWithRetries<Output>({
106
- model: {},
107
- schema,
108
- system: "sys",
109
- user: "user",
110
- strict: true,
111
- maxAttempts: 2,
112
- execute: async () => {
113
- calls += 1;
114
- return {
115
- data: {} as Output,
116
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
117
- };
118
- },
119
- })
120
- ).rejects.toThrow();
121
-
122
- expect(calls).toBe(2);
123
- });
124
-
125
- test("runWithRetries with strict=false still validates type errors", async () => {
126
- let calls = 0;
127
- const result = await runWithRetries<Output>({
128
- model: {},
129
- schema,
130
- system: "sys",
131
- user: "user",
132
- strict: false,
133
- execute: async () => {
134
- calls += 1;
135
- if (calls === 1) {
136
- return {
137
- data: { title: 123 } as unknown as Output,
138
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
139
- };
140
- }
141
- return {
142
- data: { title: "ok" },
143
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
144
- };
145
- },
146
- });
147
-
148
- expect(result.data.title).toBe("ok");
149
- expect(calls).toBe(2);
150
- });
151
-
152
- test("runWithRetries enforces strict validation on final attempt even with strict=false", async () => {
153
- let calls = 0;
154
-
155
- await expect(
156
- runWithRetries<Output>({
157
- model: {},
158
- schema,
159
- system: "sys",
160
- user: "user",
161
- strict: false,
162
- maxAttempts: 2,
163
- execute: async () => {
164
- calls += 1;
165
- return {
166
- data: {} as Output,
167
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
168
- };
169
- },
170
- })
171
- ).rejects.toThrow();
172
-
173
- expect(calls).toBe(2);
174
- });
@@ -1,270 +0,0 @@
1
- import {
2
- createAjv,
3
- validateOrThrow,
4
- SchemaValidationError,
5
- validateAllowingMissingRequired,
6
- } from "../validation/validator";
7
- import type { ModelMessage } from "ai";
8
- import type { ExtractionEvents, Usage, TelemetryAdapter } from "../types";
9
- import type { DebugLogger } from "../debug/logger";
10
- import { generateStructured } from "./LLMClient";
11
- import type { UserContent } from "./message";
12
-
13
- export type RetryOptions<T> = {
14
- model: unknown;
15
- schema: unknown;
16
- system: string;
17
- user: UserContent;
18
- events?: ExtractionEvents;
19
- maxAttempts?: number;
20
- schemaName?: string;
21
- execute?: typeof generateStructured<T>;
22
- strict?: boolean;
23
- debug?: DebugLogger;
24
- callId?: string;
25
- /**
26
- * Telemetry adapter for tracing validation and retries
27
- */
28
- telemetry?: TelemetryAdapter;
29
- /**
30
- * Parent span for creating hierarchical traces
31
- */
32
- parentSpan?: { id: string; traceId: string; name: string; kind: string; startTime: number; parentId?: string };
33
- };
34
-
35
- export const runWithRetries = async <T>(options: RetryOptions<T>) => {
36
- const { telemetry, parentSpan } = options;
37
-
38
- // Start validation/retry span if telemetry is enabled
39
- const retrySpan = telemetry?.startSpan({
40
- name: "struktur.validation_retry",
41
- kind: "CHAIN",
42
- parentSpan,
43
- attributes: {
44
- "retry.max_attempts": options.maxAttempts ?? 3,
45
- "retry.schema_name": options.schemaName ?? "extract",
46
- },
47
- });
48
-
49
- const ajv = createAjv();
50
- const maxAttempts = options.maxAttempts ?? 3;
51
- const messages: ModelMessage[] = [{ role: "user", content: options.user }];
52
- const debug = options.debug;
53
- const callId =
54
- options.callId ??
55
- `call_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
56
-
57
- let usage: Usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
58
- let lastError: Error | undefined;
59
-
60
- // Log LLM call start
61
- const systemLength = options.system.length;
62
- const userLength =
63
- typeof options.user === "string"
64
- ? options.user.length
65
- : JSON.stringify(options.user).length;
66
-
67
- debug?.llmCallStart({
68
- callId,
69
- model: JSON.stringify(options.model),
70
- schemaName: options.schemaName,
71
- systemLength,
72
- userLength,
73
- artifactCount: Array.isArray(options.user) ? options.user.length : 0,
74
- });
75
-
76
- debug?.promptSystem({ callId, system: options.system });
77
- debug?.promptUser({ callId, user: options.user });
78
-
79
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
80
- const executor = options.execute ?? generateStructured;
81
- const isFinalAttempt = attempt === maxAttempts;
82
- const useStrictValidation = options.strict === true || isFinalAttempt;
83
-
84
- debug?.validationStart({
85
- callId,
86
- attempt,
87
- maxAttempts,
88
- strict: useStrictValidation,
89
- });
90
-
91
- const startTime = Date.now();
92
- const result = await executor({
93
- model: options.model,
94
- schema: options.schema,
95
- schemaName: options.schemaName,
96
- system: options.system,
97
- user: options.user,
98
- messages,
99
- strict: options.strict,
100
- telemetry,
101
- parentSpan: retrySpan,
102
- });
103
- const durationMs = Date.now() - startTime;
104
-
105
- usage = {
106
- inputTokens: usage.inputTokens + result.usage.inputTokens,
107
- outputTokens: usage.outputTokens + result.usage.outputTokens,
108
- totalTokens: usage.totalTokens + result.usage.totalTokens,
109
- };
110
-
111
- debug?.rawResponse({ callId, response: result.data });
112
-
113
- try {
114
- if (useStrictValidation) {
115
- const validated = validateOrThrow<T>(
116
- ajv,
117
- options.schema as never,
118
- result.data,
119
- );
120
-
121
- debug?.validationSuccess({ callId, attempt });
122
- debug?.llmCallComplete({
123
- callId,
124
- success: true,
125
- inputTokens: usage.inputTokens,
126
- outputTokens: usage.outputTokens,
127
- totalTokens: usage.totalTokens,
128
- durationMs,
129
- });
130
-
131
- // Record successful validation
132
- if (retrySpan && telemetry) {
133
- telemetry.recordEvent(retrySpan, {
134
- type: "validation",
135
- attempt,
136
- maxAttempts,
137
- schema: options.schema,
138
- input: result.data,
139
- success: true,
140
- latencyMs: durationMs,
141
- });
142
- telemetry.endSpan(retrySpan, {
143
- status: "ok",
144
- output: validated,
145
- latencyMs: durationMs,
146
- });
147
- }
148
-
149
- return { data: validated, usage };
150
- } else {
151
- const validationResult = validateAllowingMissingRequired<T>(
152
- ajv,
153
- options.schema as never,
154
- result.data,
155
- isFinalAttempt,
156
- );
157
-
158
- if (validationResult.valid) {
159
- debug?.validationSuccess({ callId, attempt });
160
- debug?.llmCallComplete({
161
- callId,
162
- success: true,
163
- inputTokens: usage.inputTokens,
164
- outputTokens: usage.outputTokens,
165
- totalTokens: usage.totalTokens,
166
- durationMs,
167
- });
168
-
169
- // Record successful validation
170
- if (retrySpan && telemetry) {
171
- telemetry.recordEvent(retrySpan, {
172
- type: "validation",
173
- attempt,
174
- maxAttempts,
175
- schema: options.schema,
176
- input: result.data,
177
- success: true,
178
- latencyMs: durationMs,
179
- });
180
- telemetry.endSpan(retrySpan, {
181
- status: "ok",
182
- output: validationResult.data,
183
- latencyMs: durationMs,
184
- });
185
- }
186
-
187
- return { data: validationResult.data, usage };
188
- }
189
-
190
- throw new SchemaValidationError(
191
- "Schema validation failed",
192
- validationResult.errors,
193
- );
194
- }
195
- } catch (error) {
196
- lastError = error as Error;
197
-
198
- if (error instanceof SchemaValidationError) {
199
- debug?.validationFailed({
200
- callId,
201
- attempt,
202
- errors: error.errors,
203
- });
204
-
205
- // Record failed validation
206
- if (retrySpan && telemetry) {
207
- telemetry.recordEvent(retrySpan, {
208
- type: "validation",
209
- attempt,
210
- maxAttempts,
211
- schema: options.schema,
212
- input: result.data,
213
- success: false,
214
- errors: error.errors,
215
- latencyMs: durationMs,
216
- });
217
- }
218
-
219
- // Emit retry event before attempting retry
220
- const nextAttempt = attempt + 1;
221
- if (nextAttempt <= maxAttempts) {
222
- await options.events?.onRetry?.({
223
- attempt: nextAttempt,
224
- maxAttempts,
225
- reason: "schema_validation_failed",
226
- });
227
-
228
- debug?.retry({
229
- callId,
230
- attempt: nextAttempt,
231
- maxAttempts,
232
- reason: "schema_validation_failed",
233
- });
234
- }
235
-
236
- const errorPayload = JSON.stringify(error.errors, null, 2);
237
- const errorMessage = `<validation-errors>\n${errorPayload}\n</validation-errors>`;
238
- messages.push({ role: "user", content: errorMessage });
239
- await options.events?.onMessage?.({
240
- role: "user",
241
- content: errorMessage,
242
- });
243
- continue;
244
- }
245
-
246
- debug?.llmCallComplete({
247
- callId,
248
- success: false,
249
- inputTokens: usage.inputTokens,
250
- outputTokens: usage.outputTokens,
251
- totalTokens: usage.totalTokens,
252
- durationMs,
253
- error: (error as Error).message,
254
- });
255
-
256
- // Record error in telemetry
257
- if (retrySpan && telemetry) {
258
- telemetry.endSpan(retrySpan, {
259
- status: "error",
260
- error: error as Error,
261
- latencyMs: durationMs,
262
- });
263
- }
264
-
265
- break;
266
- }
267
- }
268
-
269
- throw lastError ?? new Error("Unknown extraction error");
270
- };
@@ -1,42 +0,0 @@
1
- import { test, expect } from "bun:test";
2
- import type { Artifact } from "../types";
3
- import { buildUserContent } from "./message";
4
-
5
- const makeArtifact = (contents: Artifact["contents"]): Artifact => ({
6
- id: "a1",
7
- type: "text",
8
- raw: async () => Buffer.from(""),
9
- contents,
10
- });
11
-
12
- test("buildUserContent returns text when no images", () => {
13
- const artifacts = [makeArtifact([{ text: "hello" }])];
14
- const content = buildUserContent("prompt", artifacts);
15
-
16
- expect(content).toBe("prompt");
17
- });
18
-
19
- test("buildUserContent appends images in order", () => {
20
- const artifacts: Artifact[] = [
21
- makeArtifact([
22
- { media: [{ type: "image", base64: "base" }] },
23
- { media: [{ type: "image", url: "https://example.com/img.png" }] },
24
- ]),
25
- {
26
- id: "a2",
27
- type: "image",
28
- raw: async () => Buffer.from(""),
29
- contents: [{ media: [{ type: "image", contents: Buffer.from([1]) }] }],
30
- },
31
- ];
32
-
33
- const content = buildUserContent("prompt", artifacts);
34
- expect(Array.isArray(content)).toBe(true);
35
-
36
- if (Array.isArray(content)) {
37
- expect(content[0]).toEqual({ type: "text", text: "prompt" });
38
- expect(content[1]).toEqual({ type: "image", image: "base" });
39
- expect(content[2]).toEqual({ type: "image", image: "https://example.com/img.png" });
40
- expect(content[3]).toEqual({ type: "image", image: Buffer.from([1]) });
41
- }
42
- });
@@ -1,47 +0,0 @@
1
- import type { Artifact } from "../types";
2
-
3
- export type ImagePart = {
4
- type: "image";
5
- image: string | Buffer;
6
- };
7
-
8
- export type TextPart = {
9
- type: "text";
10
- text: string;
11
- };
12
-
13
- export type UserContent = string | Array<TextPart | ImagePart>;
14
-
15
- const collectImages = (artifacts: Artifact[]): ImagePart[] => {
16
- const parts: ImagePart[] = [];
17
-
18
- for (const artifact of artifacts) {
19
- for (const content of artifact.contents) {
20
- if (!content.media?.length) {
21
- continue;
22
- }
23
-
24
- for (const media of content.media) {
25
- if (media.contents) {
26
- parts.push({ type: "image", image: media.contents });
27
- } else if (media.base64) {
28
- parts.push({ type: "image", image: media.base64 });
29
- } else if (media.url) {
30
- parts.push({ type: "image", image: media.url });
31
- }
32
- }
33
- }
34
- }
35
-
36
- return parts;
37
- };
38
-
39
- export const buildUserContent = (text: string, artifacts: Artifact[]): UserContent => {
40
- const images = collectImages(artifacts);
41
-
42
- if (images.length === 0) {
43
- return text;
44
- }
45
-
46
- return [{ type: "text", text }, ...images];
47
- };
@@ -1,82 +0,0 @@
1
- import { test, expect } from "bun:test";
2
- import { __testing__ } from "./models";
3
-
4
- test("parseOpenAiModels returns model ids", () => {
5
- const models = __testing__.parseOpenAiModels({
6
- object: "list",
7
- data: [{ id: "gpt-4o-mini" }, { id: "gpt-4o" }],
8
- });
9
-
10
- expect(models).toEqual(["gpt-4o-mini", "gpt-4o"]);
11
- });
12
-
13
- test("parseAnthropicModels returns model ids", () => {
14
- const models = __testing__.parseAnthropicModels({
15
- data: [{ id: "claude-3-5-sonnet-20241022" }],
16
- });
17
-
18
- expect(models).toEqual(["claude-3-5-sonnet-20241022"]);
19
- });
20
-
21
- test("parseGoogleModels strips models prefix", () => {
22
- const models = __testing__.parseGoogleModels({
23
- models: [{ name: "models/gemini-1.5-flash" }],
24
- });
25
-
26
- expect(models).toEqual(["gemini-1.5-flash"]);
27
- });
28
-
29
- test("parseOpenRouterModels returns model ids", () => {
30
- const models = __testing__.parseOpenRouterModels({
31
- data: [{ id: "openai/gpt-4o" }, { id: "anthropic/claude-3.5-sonnet" }],
32
- });
33
-
34
- expect(models).toEqual(["openai/gpt-4o", "anthropic/claude-3.5-sonnet"]);
35
- });
36
-
37
- test("parseOpenAiModels handles empty data", () => {
38
- const models = __testing__.parseOpenAiModels({});
39
- expect(models).toEqual([]);
40
- });
41
-
42
- test("parseOpenAiModels filters out undefined ids", () => {
43
- const models = __testing__.parseOpenAiModels({
44
- data: [{ id: "gpt-4" }, { notId: "bad" }],
45
- });
46
- expect(models).toEqual(["gpt-4"]);
47
- });
48
-
49
- test("parseGoogleModels handles empty models", () => {
50
- const models = __testing__.parseGoogleModels({});
51
- expect(models).toEqual([]);
52
- });
53
-
54
- test("pickCheapestModel prefers known cheap models", () => {
55
- const models = ["gpt-4o", "gpt-4o-mini"];
56
- expect(__testing__.pickCheapestModel("openai", models)).toBe("gpt-4o-mini");
57
- });
58
-
59
- test("pickCheapestModel returns first model if no preference matches", () => {
60
- const models = ["unknown-model-1", "unknown-model-2"];
61
- expect(__testing__.pickCheapestModel("openai", models)).toBe("unknown-model-1");
62
- });
63
-
64
- test("pickCheapestModel matches prefix for versioned models", () => {
65
- const models = ["gpt-4o-mini-2024-07-18", "gpt-4o-2024-05-13"];
66
- expect(__testing__.pickCheapestModel("openai", models)).toBe("gpt-4o-mini-2024-07-18");
67
- });
68
-
69
- test("pickCheapestModel handles anthropic preferences", () => {
70
- const models = ["claude-3-opus", "claude-3-5-haiku-20241022"];
71
- expect(__testing__.pickCheapestModel("anthropic", models)).toBe("claude-3-5-haiku-20241022");
72
- });
73
-
74
- test("pickCheapestModel handles google preferences", () => {
75
- const models = ["gemini-1.5-pro", "gemini-2.0-flash"];
76
- expect(__testing__.pickCheapestModel("google", models)).toBe("gemini-2.0-flash");
77
- });
78
-
79
- test("pickCheapestModel handles unknown provider", () => {
80
- const models = ["model-a", "model-b"];
81
- expect(__testing__.pickCheapestModel("unknown", models)).toBe("model-a");
82
- });