avantgate 1.0.0 → 1.1.1
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 +580 -287
- package/dist/agent/index.d.mts +704 -0
- package/dist/agent/index.d.ts +704 -0
- package/dist/agent/index.js +1724 -0
- package/dist/agent/index.mjs +1623 -0
- package/dist/chunk-CO26LNFD.mjs +521 -0
- package/dist/chunk-DVCF4CSV.mjs +56 -0
- package/dist/finance/index.d.mts +76 -0
- package/dist/finance/index.d.ts +76 -0
- package/dist/finance/index.js +536 -0
- package/dist/finance/index.mjs +20 -0
- package/dist/index.d.mts +215 -9
- package/dist/index.d.ts +215 -9
- package/dist/index.js +1356 -59
- package/dist/index.mjs +845 -113
- package/dist/strategy.interface-CB4_ZAuk.d.mts +16 -0
- package/dist/strategy.interface-CB4_ZAuk.d.ts +16 -0
- package/package.json +82 -55
package/dist/index.js
CHANGED
|
@@ -20,61 +20,236 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
AvantGateBudgetExceededError: () => BudgetExceededError,
|
|
24
|
+
AvantGateConfigurationError: () => ConfigurationError,
|
|
23
25
|
AvantGateControlLayer: () => AvantGateControlLayer,
|
|
26
|
+
BudgetExceededError: () => BudgetExceededError,
|
|
27
|
+
CachedPricingAdapter: () => CachedPricingAdapter,
|
|
28
|
+
ConfigurationError: () => ConfigurationError,
|
|
24
29
|
DEFAULT_MODEL_PRICES: () => DEFAULT_MODEL_PRICES,
|
|
30
|
+
HttpProviderClient: () => HttpProviderClient,
|
|
31
|
+
PricingRegistry: () => PricingRegistry,
|
|
32
|
+
PromptBuilder: () => PromptBuilder,
|
|
33
|
+
PromptRegistry: () => PromptRegistry,
|
|
34
|
+
PromptTemplate: () => PromptTemplate,
|
|
35
|
+
SEED_MODEL_PRICES: () => SEED_MODEL_PRICES,
|
|
25
36
|
ZenLLMControlLayer: () => AvantGateControlLayer,
|
|
26
37
|
calculateCostUSD: () => calculateCostUSD,
|
|
27
38
|
createAvantGate: () => createAvantGate,
|
|
39
|
+
createHttpProviderClient: () => createHttpProviderClient,
|
|
28
40
|
createLLMControlLayer: () => createLLMControlLayer,
|
|
29
41
|
extractAndCleanJSON: () => extractAndCleanJSON,
|
|
42
|
+
resolveModelPrice: () => resolveModelPrice,
|
|
43
|
+
resolveModelPriceAsync: () => resolveModelPriceAsync,
|
|
30
44
|
sanitizePII: () => sanitizePII,
|
|
31
45
|
validateUserInput: () => validateUserInput,
|
|
32
46
|
validateWithZod: () => validateWithZod
|
|
33
47
|
});
|
|
34
48
|
module.exports = __toCommonJS(index_exports);
|
|
35
49
|
|
|
50
|
+
// src/types.ts
|
|
51
|
+
var ConfigurationError = class extends Error {
|
|
52
|
+
constructor(message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "ConfigurationError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var BudgetExceededError = class extends Error {
|
|
58
|
+
constructor(message) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "BudgetExceededError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
36
64
|
// src/pricing.ts
|
|
37
|
-
var
|
|
38
|
-
// DeepSeek
|
|
39
|
-
"deepseek-chat": {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
var SEED_MODEL_PRICES = {
|
|
66
|
+
// DeepSeek Direct
|
|
67
|
+
"deepseek-chat": { promptUSDPerMillion: 0.14, completionUSDPerMillion: 0.28, cacheHitUSDPerMillion: 0.014 },
|
|
68
|
+
"deepseek-reasoner": { promptUSDPerMillion: 0.55, completionUSDPerMillion: 2.19, cacheHitUSDPerMillion: 0.14 },
|
|
69
|
+
"deepseek/deepseek-chat": { promptUSDPerMillion: 0.14, completionUSDPerMillion: 0.28, cacheHitUSDPerMillion: 0.014 },
|
|
70
|
+
"deepseek/deepseek-reasoner": { promptUSDPerMillion: 0.55, completionUSDPerMillion: 2.19, cacheHitUSDPerMillion: 0.14 },
|
|
71
|
+
// Mistral AI Direct
|
|
72
|
+
"mistral-small-latest": { promptUSDPerMillion: 0.2, completionUSDPerMillion: 0.6 },
|
|
73
|
+
"mistral-large-latest": { promptUSDPerMillion: 2, completionUSDPerMillion: 6 },
|
|
74
|
+
"mistral/mistral-small-latest": { promptUSDPerMillion: 0.2, completionUSDPerMillion: 0.6 },
|
|
75
|
+
"mistral/mistral-large-latest": { promptUSDPerMillion: 2, completionUSDPerMillion: 6 },
|
|
76
|
+
// OpenAI Direct
|
|
77
|
+
"gpt-4o-mini": { promptUSDPerMillion: 0.15, completionUSDPerMillion: 0.6 },
|
|
78
|
+
"gpt-4o": { promptUSDPerMillion: 2.5, completionUSDPerMillion: 10 },
|
|
79
|
+
"openai/gpt-4o-mini": { promptUSDPerMillion: 0.15, completionUSDPerMillion: 0.6 },
|
|
80
|
+
"openai/gpt-4o": { promptUSDPerMillion: 2.5, completionUSDPerMillion: 10 },
|
|
81
|
+
// OpenRouter Direct
|
|
82
|
+
"openrouter/deepseek/deepseek-chat": { promptUSDPerMillion: 0.14, completionUSDPerMillion: 0.28 },
|
|
83
|
+
"openrouter/deepseek/deepseek-r1": { promptUSDPerMillion: 0.55, completionUSDPerMillion: 2.19 },
|
|
84
|
+
"openrouter/anthropic/claude-3.5-sonnet": { promptUSDPerMillion: 3, completionUSDPerMillion: 15 },
|
|
85
|
+
// Local Ollama (Toujours $0)
|
|
86
|
+
"ollama": { promptUSDPerMillion: 0, completionUSDPerMillion: 0 }
|
|
87
|
+
};
|
|
88
|
+
function normalizeKey(identifier) {
|
|
89
|
+
return identifier.trim().toLowerCase().replace(":", "/");
|
|
90
|
+
}
|
|
91
|
+
var CachedPricingAdapter = class {
|
|
92
|
+
cache = /* @__PURE__ */ new Map();
|
|
93
|
+
ttlMs;
|
|
94
|
+
delegate;
|
|
95
|
+
constructor(delegate, ttlMs = 5 * 60 * 1e3) {
|
|
96
|
+
this.delegate = delegate;
|
|
97
|
+
this.ttlMs = ttlMs;
|
|
98
|
+
}
|
|
99
|
+
async fetchPrice(model, provider) {
|
|
100
|
+
const key = provider ? normalizeKey(`${provider}/${model}`) : normalizeKey(model);
|
|
101
|
+
const cached = this.cache.get(key);
|
|
102
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
103
|
+
return cached.price;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const freshPrice = await this.delegate.fetchPrice(model, provider);
|
|
107
|
+
if (freshPrice) {
|
|
108
|
+
this.cache.set(key, { price: freshPrice, expiresAt: Date.now() + this.ttlMs });
|
|
109
|
+
return freshPrice;
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
if (cached) {
|
|
113
|
+
return cached.price;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return void 0;
|
|
117
|
+
}
|
|
118
|
+
peek(model, provider) {
|
|
119
|
+
const key = provider ? normalizeKey(`${provider}/${model}`) : normalizeKey(model);
|
|
120
|
+
return this.cache.get(key)?.price;
|
|
121
|
+
}
|
|
122
|
+
clearCache() {
|
|
123
|
+
this.cache.clear();
|
|
124
|
+
}
|
|
125
|
+
invalidate(model, provider) {
|
|
126
|
+
if (!model) {
|
|
127
|
+
this.clearCache();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const key = provider ? normalizeKey(`${provider}/${model}`) : normalizeKey(model);
|
|
131
|
+
this.cache.delete(key);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
var PricingRegistry = class {
|
|
135
|
+
static prices = /* @__PURE__ */ new Map();
|
|
136
|
+
static adapter;
|
|
137
|
+
static registerPrice(identifier, price) {
|
|
138
|
+
this.prices.set(normalizeKey(identifier), price);
|
|
139
|
+
}
|
|
140
|
+
static registerDistributorPrices(distributor, priceMap) {
|
|
141
|
+
for (const [model, price] of Object.entries(priceMap)) {
|
|
142
|
+
this.prices.set(normalizeKey(`${distributor}/${model}`), price);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
static registerPrices(prices) {
|
|
146
|
+
for (const [key, price] of Object.entries(prices)) {
|
|
147
|
+
this.prices.set(normalizeKey(key), price);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
static getPrice(model, provider) {
|
|
151
|
+
if (provider) {
|
|
152
|
+
const distributorKey = normalizeKey(`${provider}/${model}`);
|
|
153
|
+
const directDistributorPrice = this.prices.get(distributorKey);
|
|
154
|
+
if (directDistributorPrice) {
|
|
155
|
+
return directDistributorPrice;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return this.prices.get(normalizeKey(model));
|
|
159
|
+
}
|
|
160
|
+
static setAdapter(adapter) {
|
|
161
|
+
this.adapter = adapter;
|
|
162
|
+
}
|
|
163
|
+
static getAdapter() {
|
|
164
|
+
return this.adapter;
|
|
165
|
+
}
|
|
166
|
+
static clear() {
|
|
167
|
+
this.prices.clear();
|
|
168
|
+
this.adapter = void 0;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Réinitialise le registre en chargeant le catalogue d'exemple SEED_MODEL_PRICES.
|
|
172
|
+
*/
|
|
173
|
+
static loadSeedPrices() {
|
|
174
|
+
this.registerPrices(SEED_MODEL_PRICES);
|
|
175
|
+
}
|
|
176
|
+
static getAllPrices() {
|
|
177
|
+
const out = {};
|
|
178
|
+
for (const [k, v] of this.prices.entries()) {
|
|
179
|
+
out[k] = v;
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
var DEFAULT_MODEL_PRICES = new Proxy({}, {
|
|
185
|
+
get(_target, prop) {
|
|
186
|
+
return PricingRegistry.getPrice(prop);
|
|
57
187
|
},
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
promptUSDPerMillion: 0.15,
|
|
61
|
-
completionUSDPerMillion: 0.6
|
|
188
|
+
has(_target, prop) {
|
|
189
|
+
return Boolean(PricingRegistry.getPrice(prop));
|
|
62
190
|
},
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
completionUSDPerMillion: 10
|
|
191
|
+
ownKeys() {
|
|
192
|
+
return Object.keys(PricingRegistry.getAllPrices());
|
|
66
193
|
},
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
194
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
195
|
+
const price = PricingRegistry.getPrice(prop);
|
|
196
|
+
if (price) {
|
|
197
|
+
return { value: price, enumerable: true, configurable: true, writable: false };
|
|
198
|
+
}
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
function resolveModelPrice(model, options) {
|
|
203
|
+
const opts = typeof options === "string" ? { provider: options } : options ?? {};
|
|
204
|
+
if (opts.providerPricing) {
|
|
205
|
+
return opts.providerPricing;
|
|
206
|
+
}
|
|
207
|
+
if (opts.customPricing) {
|
|
208
|
+
if (opts.provider) {
|
|
209
|
+
const customDistributorKey = normalizeKey(`${opts.provider}/${model}`);
|
|
210
|
+
if (opts.customPricing[customDistributorKey]) {
|
|
211
|
+
return opts.customPricing[customDistributorKey];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (opts.customPricing[normalizeKey(model)] ?? opts.customPricing[model]) {
|
|
215
|
+
return opts.customPricing[normalizeKey(model)] ?? opts.customPricing[model];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const activeAdapter = opts.adapter ?? PricingRegistry.getAdapter();
|
|
219
|
+
if (activeAdapter instanceof CachedPricingAdapter) {
|
|
220
|
+
const cached = activeAdapter.peek(model, opts.provider);
|
|
221
|
+
if (cached) {
|
|
222
|
+
return cached;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const registeredPrice = PricingRegistry.getPrice(model, opts.provider);
|
|
226
|
+
if (registeredPrice) {
|
|
227
|
+
return registeredPrice;
|
|
228
|
+
}
|
|
229
|
+
if (opts.provider === "ollama" || model.toLowerCase().includes("ollama")) {
|
|
230
|
+
return { promptUSDPerMillion: 0, completionUSDPerMillion: 0 };
|
|
231
|
+
}
|
|
232
|
+
return void 0;
|
|
233
|
+
}
|
|
234
|
+
async function resolveModelPriceAsync(model, options) {
|
|
235
|
+
const opts = typeof options === "string" ? { provider: options } : options ?? {};
|
|
236
|
+
const activeAdapter = opts.adapter ?? PricingRegistry.getAdapter();
|
|
237
|
+
if (activeAdapter) {
|
|
238
|
+
try {
|
|
239
|
+
const adapterPrice = await activeAdapter.fetchPrice(model, opts.provider);
|
|
240
|
+
if (adapterPrice) {
|
|
241
|
+
return adapterPrice;
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return resolveModelPrice(model, options);
|
|
247
|
+
}
|
|
248
|
+
function calculateCostUSD(model, promptTokens, completionTokens, cacheHitTokens = 0, options) {
|
|
249
|
+
const pricing = resolveModelPrice(model, options);
|
|
250
|
+
if (!pricing) {
|
|
251
|
+
return 0;
|
|
71
252
|
}
|
|
72
|
-
};
|
|
73
|
-
function calculateCostUSD(model, promptTokens, completionTokens, cacheHitTokens = 0) {
|
|
74
|
-
const pricing = DEFAULT_MODEL_PRICES[model] ?? {
|
|
75
|
-
promptUSDPerMillion: 0.5,
|
|
76
|
-
completionUSDPerMillion: 1.5
|
|
77
|
-
};
|
|
78
253
|
const promptCost = promptTokens / 1e6 * pricing.promptUSDPerMillion;
|
|
79
254
|
const completionCost = completionTokens / 1e6 * pricing.completionUSDPerMillion;
|
|
80
255
|
const cacheDiscount = cacheHitTokens && pricing.cacheHitUSDPerMillion ? cacheHitTokens / 1e6 * (pricing.promptUSDPerMillion - pricing.cacheHitUSDPerMillion) : 0;
|
|
@@ -84,8 +259,11 @@ function calculateCostUSD(model, promptTokens, completionTokens, cacheHitTokens
|
|
|
84
259
|
// src/sanitizer.ts
|
|
85
260
|
var EMAIL_REGEX = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b/g;
|
|
86
261
|
var PHONE_FR_REGEX = /\b(?:(?:\+|00)33|0)\s*[1-9](?:[\s.-]*\d{2}){4}\b/g;
|
|
87
|
-
var NIR_SSN_REGEX = /\b[12]\s*\d{2}\s
|
|
88
|
-
var
|
|
262
|
+
var NIR_SSN_REGEX = /\b[12]\s*\d{2}\s*(?:0[1-9]|1[0-2]|[2-9]\d)\s*(?:0[1-9]|[1-8]\d|9[0-8]|2[ABab])\s*(?!000)\d{3}\s*(?!000)\d{3}(?:\s*\d{2})?\b/g;
|
|
263
|
+
var SPI_LABELLED_REGEX = /(?:(?:num[ée]ro\s+fiscal|spi|n[°o]\s*fiscal|d[ée]clarant(?: fiscal)?)\s*[:=]?\s*)\b(\d{2}(?:[\s.-]?\d{2}){5}[\s.-]?\d|\d{13})\b/gi;
|
|
264
|
+
var SPI_FORMATTED_REGEX = /\b[0-3]\d(?:\s+\d{2}){5}\s+\d\b/g;
|
|
265
|
+
var IBAN_REGEX = /\b[A-Z]{2}\s*[0-9]{2}(?:[\s\r\n.-]*[A-Z0-9]){11,30}\b/g;
|
|
266
|
+
var BIC_LABELLED_REGEX = /(?:(?:bic|swift)\s*[:=]?\s*)\b([A-Z]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?)\b/gi;
|
|
89
267
|
function sanitizePII(input) {
|
|
90
268
|
let count = 0;
|
|
91
269
|
let result = input;
|
|
@@ -97,13 +275,36 @@ function sanitizePII(input) {
|
|
|
97
275
|
count++;
|
|
98
276
|
return "[REDACTED_PHONE]";
|
|
99
277
|
});
|
|
100
|
-
result = result.replace(
|
|
278
|
+
result = result.replace(IBAN_REGEX, (match) => {
|
|
279
|
+
const cleanChars = match.replace(/[\s\r\n.-]/g, "");
|
|
280
|
+
if (cleanChars.length >= 15 && cleanChars.length <= 34) {
|
|
281
|
+
count++;
|
|
282
|
+
return "[REDACTED_IBAN]";
|
|
283
|
+
}
|
|
284
|
+
return match;
|
|
285
|
+
});
|
|
286
|
+
result = result.replace(BIC_LABELLED_REGEX, (_, bicCode) => {
|
|
101
287
|
count++;
|
|
102
|
-
return
|
|
288
|
+
return `[REDACTED_BIC: ${bicCode.slice(0, 4)}****]`;
|
|
103
289
|
});
|
|
104
|
-
result = result.replace(
|
|
290
|
+
result = result.replace(SPI_LABELLED_REGEX, (fullMatch, digits) => {
|
|
105
291
|
count++;
|
|
106
|
-
return "[
|
|
292
|
+
return fullMatch.replace(digits, "[REDACTED_SPI]");
|
|
293
|
+
});
|
|
294
|
+
result = result.replace(NIR_SSN_REGEX, (match) => {
|
|
295
|
+
const rawDigits = match.replace(/\s+/g, "");
|
|
296
|
+
if (rawDigits.length === 13 || rawDigits.length === 15) {
|
|
297
|
+
count++;
|
|
298
|
+
return "[REDACTED_NIR]";
|
|
299
|
+
}
|
|
300
|
+
return match;
|
|
301
|
+
});
|
|
302
|
+
result = result.replace(SPI_FORMATTED_REGEX, (match) => {
|
|
303
|
+
if (!match.includes("[REDACTED")) {
|
|
304
|
+
count++;
|
|
305
|
+
return "[REDACTED_SPI]";
|
|
306
|
+
}
|
|
307
|
+
return match;
|
|
107
308
|
});
|
|
108
309
|
return { text: result, maskedCount: count };
|
|
109
310
|
}
|
|
@@ -138,6 +339,469 @@ function validateUserInput(input, options) {
|
|
|
138
339
|
return { valid: true };
|
|
139
340
|
}
|
|
140
341
|
|
|
342
|
+
// src/finance/strategies/french-pcg.strategy.ts
|
|
343
|
+
var FrenchPCGStrategy = class {
|
|
344
|
+
jurisdictionCode = "FR";
|
|
345
|
+
standard = "PCG";
|
|
346
|
+
defaultCurrency = "EUR";
|
|
347
|
+
cleanNumber(value) {
|
|
348
|
+
if (typeof value === "number") {
|
|
349
|
+
return Number.isFinite(value) ? value : 0;
|
|
350
|
+
}
|
|
351
|
+
if (!value || typeof value !== "string") {
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
let str = value.trim();
|
|
355
|
+
let isNegative = false;
|
|
356
|
+
const parenMatch = str.match(/^\(\s*(.+?)\s*\)$/);
|
|
357
|
+
if (parenMatch) {
|
|
358
|
+
isNegative = true;
|
|
359
|
+
str = parenMatch[1];
|
|
360
|
+
} else if (str.startsWith("-")) {
|
|
361
|
+
isNegative = true;
|
|
362
|
+
str = str.substring(1).trim();
|
|
363
|
+
}
|
|
364
|
+
let multiplier = 1;
|
|
365
|
+
if (/[kK](?:€|eur)?\b/i.test(str)) {
|
|
366
|
+
multiplier = 1e3;
|
|
367
|
+
str = str.replace(/[kK](?:€|eur)?\b/gi, "").trim();
|
|
368
|
+
} else if (/[mM](?:€|eur)?\b/i.test(str)) {
|
|
369
|
+
multiplier = 1e6;
|
|
370
|
+
str = str.replace(/[mM](?:€|eur)?\b/gi, "").trim();
|
|
371
|
+
}
|
|
372
|
+
str = str.replace(/[€$£]/g, "").replace(/\s+/g, "").replace(/\u00A0/g, "").trim();
|
|
373
|
+
str = str.replace(",", ".");
|
|
374
|
+
const parsed = parseFloat(str);
|
|
375
|
+
if (isNaN(parsed)) {
|
|
376
|
+
return 0;
|
|
377
|
+
}
|
|
378
|
+
const finalValue = parsed * multiplier;
|
|
379
|
+
return isNegative ? -Math.abs(finalValue) : finalValue;
|
|
380
|
+
}
|
|
381
|
+
cleanJSON(rawText) {
|
|
382
|
+
let result = rawText;
|
|
383
|
+
result = result.replace(
|
|
384
|
+
/(:\s*)\(\s*([0-9][0-9\s.,]*(?:k€|k|keur|m€|m|meur|€)?)\s*\)/gi,
|
|
385
|
+
(_, prefix, amountStr) => {
|
|
386
|
+
const num = this.cleanNumber(`(${amountStr})`);
|
|
387
|
+
return `${prefix}${num}`;
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
result = result.replace(
|
|
391
|
+
/(:\s*)"\s*\(\s*([0-9][0-9\s.,]*(?:k€|k|keur|m€|m|meur|€)?)\s*\)\s*"/gi,
|
|
392
|
+
(_, prefix, amountStr) => {
|
|
393
|
+
const num = this.cleanNumber(`(${amountStr})`);
|
|
394
|
+
return `${prefix}${num}`;
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
result = result.replace(
|
|
398
|
+
/(:\s*)"\s*([+-]?[0-9][0-9\s.,]*(?:k€|k|keur|m€|m|meur|€))\s*"/gi,
|
|
399
|
+
(_, prefix, amountStr) => {
|
|
400
|
+
const num = this.cleanNumber(amountStr);
|
|
401
|
+
return `${prefix}${num}`;
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
result = result.replace(
|
|
405
|
+
/(:\s*)"\s*([+-]?(?:[0-9]{1,3}(?:\s+[0-9]{3})+(?:,[0-9]+)?|[0-9]+,[0-9]+))\s*"/g,
|
|
406
|
+
(_, prefix, amountStr) => {
|
|
407
|
+
const num = this.cleanNumber(amountStr);
|
|
408
|
+
return `${prefix}${num}`;
|
|
409
|
+
}
|
|
410
|
+
);
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
detect(text) {
|
|
414
|
+
const lower = text.toLowerCase();
|
|
415
|
+
const frPatterns = [
|
|
416
|
+
/\bcerfa\b/i,
|
|
417
|
+
/\bliasse\s+fiscale\b/i,
|
|
418
|
+
/\bbilan\s+(actif|passif)\b/i,
|
|
419
|
+
/\bcompte\s+de\s+r[ée]sultat\b/i,
|
|
420
|
+
/\bplan\s+comptable\s+g[ée]n[ée]ral\b/i,
|
|
421
|
+
/\bpcg\b/i,
|
|
422
|
+
/\bsiren\b/i,
|
|
423
|
+
/\bsiret\b/i,
|
|
424
|
+
/\b[0-9\s.,]+(?:k€|m€)\b/i,
|
|
425
|
+
/\beur\b/i,
|
|
426
|
+
/€/
|
|
427
|
+
];
|
|
428
|
+
return frPatterns.some((pattern) => pattern.test(lower));
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// src/finance/strategies/us-gaap.strategy.ts
|
|
433
|
+
var UsGAAPStrategy = class {
|
|
434
|
+
jurisdictionCode = "US";
|
|
435
|
+
standard = "US_GAAP";
|
|
436
|
+
defaultCurrency = "USD";
|
|
437
|
+
cleanNumber(value) {
|
|
438
|
+
if (typeof value === "number") {
|
|
439
|
+
return Number.isFinite(value) ? value : 0;
|
|
440
|
+
}
|
|
441
|
+
if (!value || typeof value !== "string") {
|
|
442
|
+
return 0;
|
|
443
|
+
}
|
|
444
|
+
let str = value.trim();
|
|
445
|
+
let isNegative = false;
|
|
446
|
+
const parenMatch = str.match(/^[(\[]\s*(.+?)\s*[)\]]$/);
|
|
447
|
+
if (parenMatch) {
|
|
448
|
+
isNegative = true;
|
|
449
|
+
str = parenMatch[1];
|
|
450
|
+
} else if (str.startsWith("-")) {
|
|
451
|
+
isNegative = true;
|
|
452
|
+
str = str.substring(1).trim();
|
|
453
|
+
}
|
|
454
|
+
let multiplier = 1;
|
|
455
|
+
if (/[kK]\b/.test(str)) {
|
|
456
|
+
multiplier = 1e3;
|
|
457
|
+
str = str.replace(/[kK]\b/g, "").trim();
|
|
458
|
+
} else if (/[mM]\b/.test(str)) {
|
|
459
|
+
multiplier = 1e6;
|
|
460
|
+
str = str.replace(/[mM]\b/g, "").trim();
|
|
461
|
+
} else if (/[bB]\b/.test(str)) {
|
|
462
|
+
multiplier = 1e9;
|
|
463
|
+
str = str.replace(/[bB]\b/g, "").trim();
|
|
464
|
+
}
|
|
465
|
+
str = str.replace(/[$]/g, "").replace(/\busd\b/gi, "").replace(/,/g, "").replace(/\s+/g, "").trim();
|
|
466
|
+
const parsed = parseFloat(str);
|
|
467
|
+
if (isNaN(parsed)) {
|
|
468
|
+
return 0;
|
|
469
|
+
}
|
|
470
|
+
const finalVal = parsed * multiplier;
|
|
471
|
+
return isNegative ? -Math.abs(finalVal) : finalVal;
|
|
472
|
+
}
|
|
473
|
+
cleanJSON(rawText) {
|
|
474
|
+
let result = rawText;
|
|
475
|
+
result = result.replace(
|
|
476
|
+
/(:\s*)[(\[]\s*([0-9][0-9,.]*(?:k|m|b|\$)?)\s*[)\]]/gi,
|
|
477
|
+
(_, prefix, amountStr) => {
|
|
478
|
+
const num = this.cleanNumber(`(${amountStr})`);
|
|
479
|
+
return `${prefix}${num}`;
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
result = result.replace(
|
|
483
|
+
/(:\s*)"\s*[(\[]\s*([0-9][0-9,.]*(?:k|m|b|\$)?)\s*[)\]]\s*"/gi,
|
|
484
|
+
(_, prefix, amountStr) => {
|
|
485
|
+
const num = this.cleanNumber(`(${amountStr})`);
|
|
486
|
+
return `${prefix}${num}`;
|
|
487
|
+
}
|
|
488
|
+
);
|
|
489
|
+
result = result.replace(
|
|
490
|
+
/(:\s*)"\s*([+-]?\$?\s*[0-9][0-9,.]*\s*(?:k|m|b|\$)?)\s*"/gi,
|
|
491
|
+
(_, prefix, amountStr) => {
|
|
492
|
+
if (!amountStr.includes("$") && !/[kmb]/i.test(amountStr)) {
|
|
493
|
+
return `${prefix}"${amountStr}"`;
|
|
494
|
+
}
|
|
495
|
+
const num = this.cleanNumber(amountStr);
|
|
496
|
+
return `${prefix}${num}`;
|
|
497
|
+
}
|
|
498
|
+
);
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
detect(text) {
|
|
502
|
+
const lower = text.toLowerCase();
|
|
503
|
+
const usPatterns = [
|
|
504
|
+
/\b10-[kq]\b/i,
|
|
505
|
+
/\bsec\s+filing\b/i,
|
|
506
|
+
/\bus[\s_-]?gaap\b/i,
|
|
507
|
+
/\bbalance\s+sheet\b/i,
|
|
508
|
+
/\bincome\s+statement\b/i,
|
|
509
|
+
/\bstatement\s+of\s+cash\s+flows\b/i,
|
|
510
|
+
/\$|usd/i
|
|
511
|
+
];
|
|
512
|
+
return usPatterns.some((pattern) => pattern.test(lower));
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/finance/strategies/uk-ifrs.strategy.ts
|
|
517
|
+
var UkIFRSStrategy = class {
|
|
518
|
+
jurisdictionCode = "UK";
|
|
519
|
+
standard = "IFRS";
|
|
520
|
+
defaultCurrency = "GBP";
|
|
521
|
+
cleanNumber(value) {
|
|
522
|
+
if (typeof value === "number") {
|
|
523
|
+
return Number.isFinite(value) ? value : 0;
|
|
524
|
+
}
|
|
525
|
+
if (!value || typeof value !== "string") {
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
let str = value.trim();
|
|
529
|
+
let isNegative = false;
|
|
530
|
+
const parenMatch = str.match(/^\(\s*(.+?)\s*\)$/);
|
|
531
|
+
if (parenMatch) {
|
|
532
|
+
isNegative = true;
|
|
533
|
+
str = parenMatch[1];
|
|
534
|
+
} else if (str.startsWith("-")) {
|
|
535
|
+
isNegative = true;
|
|
536
|
+
str = str.substring(1).trim();
|
|
537
|
+
}
|
|
538
|
+
let multiplier = 1;
|
|
539
|
+
if (/[kK]\b/.test(str)) {
|
|
540
|
+
multiplier = 1e3;
|
|
541
|
+
str = str.replace(/[kK]\b/g, "").trim();
|
|
542
|
+
} else if (/[mM]\b/.test(str)) {
|
|
543
|
+
multiplier = 1e6;
|
|
544
|
+
str = str.replace(/[mM]\b/g, "").trim();
|
|
545
|
+
}
|
|
546
|
+
str = str.replace(/[£]/g, "").replace(/\bgbp\b/gi, "").replace(/,/g, "").replace(/\s+/g, "").trim();
|
|
547
|
+
const parsed = parseFloat(str);
|
|
548
|
+
if (isNaN(parsed)) {
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
551
|
+
const finalVal = parsed * multiplier;
|
|
552
|
+
return isNegative ? -Math.abs(finalVal) : finalVal;
|
|
553
|
+
}
|
|
554
|
+
cleanJSON(rawText) {
|
|
555
|
+
let result = rawText;
|
|
556
|
+
result = result.replace(
|
|
557
|
+
/(:\s*)\(\s*([0-9][0-9,.]*(?:k|m|£)?)\s*\)/gi,
|
|
558
|
+
(_, prefix, amountStr) => {
|
|
559
|
+
const num = this.cleanNumber(`(${amountStr})`);
|
|
560
|
+
return `${prefix}${num}`;
|
|
561
|
+
}
|
|
562
|
+
);
|
|
563
|
+
result = result.replace(
|
|
564
|
+
/(:\s*)"\s*\(\s*([0-9][0-9,.]*(?:k|m|£)?)\s*\)\s*"/gi,
|
|
565
|
+
(_, prefix, amountStr) => {
|
|
566
|
+
const num = this.cleanNumber(`(${amountStr})`);
|
|
567
|
+
return `${prefix}${num}`;
|
|
568
|
+
}
|
|
569
|
+
);
|
|
570
|
+
result = result.replace(
|
|
571
|
+
/(:\s*)"\s*([+-]?£\s*[0-9][0-9,.]*\s*(?:k|m)?)\s*"/gi,
|
|
572
|
+
(_, prefix, amountStr) => {
|
|
573
|
+
const num = this.cleanNumber(amountStr);
|
|
574
|
+
return `${prefix}${num}`;
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
return result;
|
|
578
|
+
}
|
|
579
|
+
detect(text) {
|
|
580
|
+
const lower = text.toLowerCase();
|
|
581
|
+
const ukPatterns = [
|
|
582
|
+
/\bcompanies\s+house\b/i,
|
|
583
|
+
/\bfrs\s*102\b/i,
|
|
584
|
+
/\bhmrc\b/i,
|
|
585
|
+
/\bprofit\s+and\s+loss\b/i,
|
|
586
|
+
/£|\bgbp\b/i
|
|
587
|
+
];
|
|
588
|
+
return ukPatterns.some((pattern) => pattern.test(lower));
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// src/finance/strategies/swiss-co.strategy.ts
|
|
593
|
+
var SwissCOStrategy = class {
|
|
594
|
+
jurisdictionCode = "CH";
|
|
595
|
+
standard = "SWISS_CO";
|
|
596
|
+
defaultCurrency = "CHF";
|
|
597
|
+
cleanNumber(value) {
|
|
598
|
+
if (typeof value === "number") {
|
|
599
|
+
return Number.isFinite(value) ? value : 0;
|
|
600
|
+
}
|
|
601
|
+
if (!value || typeof value !== "string") {
|
|
602
|
+
return 0;
|
|
603
|
+
}
|
|
604
|
+
let str = value.trim();
|
|
605
|
+
let isNegative = false;
|
|
606
|
+
const parenMatch = str.match(/^\(\s*(.+?)\s*\)$/);
|
|
607
|
+
if (parenMatch) {
|
|
608
|
+
isNegative = true;
|
|
609
|
+
str = parenMatch[1];
|
|
610
|
+
} else if (str.startsWith("-")) {
|
|
611
|
+
isNegative = true;
|
|
612
|
+
str = str.substring(1).trim();
|
|
613
|
+
}
|
|
614
|
+
let multiplier = 1;
|
|
615
|
+
if (/[kK]\b/.test(str)) {
|
|
616
|
+
multiplier = 1e3;
|
|
617
|
+
str = str.replace(/[kK]\b/g, "").trim();
|
|
618
|
+
} else if (/[mM]\b/.test(str)) {
|
|
619
|
+
multiplier = 1e6;
|
|
620
|
+
str = str.replace(/[mM]\b/g, "").trim();
|
|
621
|
+
}
|
|
622
|
+
str = str.replace(/\bchf\b/gi, "").replace(/['’]/g, "").replace(/\s+/g, "").trim();
|
|
623
|
+
str = str.replace(",", ".");
|
|
624
|
+
const parsed = parseFloat(str);
|
|
625
|
+
if (isNaN(parsed)) {
|
|
626
|
+
return 0;
|
|
627
|
+
}
|
|
628
|
+
const finalVal = parsed * multiplier;
|
|
629
|
+
return isNegative ? -Math.abs(finalVal) : finalVal;
|
|
630
|
+
}
|
|
631
|
+
cleanJSON(rawText) {
|
|
632
|
+
let result = rawText;
|
|
633
|
+
result = result.replace(
|
|
634
|
+
/(:\s*)\(\s*([0-9][0-9'’.,\s]*(?:k|m|chf)?)\s*\)/gi,
|
|
635
|
+
(_, prefix, amountStr) => {
|
|
636
|
+
const num = this.cleanNumber(`(${amountStr})`);
|
|
637
|
+
return `${prefix}${num}`;
|
|
638
|
+
}
|
|
639
|
+
);
|
|
640
|
+
result = result.replace(
|
|
641
|
+
/(:\s*)"\s*\(\s*([0-9][0-9'’.,\s]*(?:k|m|chf)?)\s*\)\s*"/gi,
|
|
642
|
+
(_, prefix, amountStr) => {
|
|
643
|
+
const num = this.cleanNumber(`(${amountStr})`);
|
|
644
|
+
return `${prefix}${num}`;
|
|
645
|
+
}
|
|
646
|
+
);
|
|
647
|
+
result = result.replace(
|
|
648
|
+
/(:\s*)"\s*([+-]?[0-9][0-9'’.,\s]*(?:k|m|chf))\s*"/gi,
|
|
649
|
+
(_, prefix, amountStr) => {
|
|
650
|
+
const num = this.cleanNumber(amountStr);
|
|
651
|
+
return `${prefix}${num}`;
|
|
652
|
+
}
|
|
653
|
+
);
|
|
654
|
+
return result;
|
|
655
|
+
}
|
|
656
|
+
detect(text) {
|
|
657
|
+
const lower = text.toLowerCase();
|
|
658
|
+
const chPatterns = [
|
|
659
|
+
/\bcode\s+des\s+obligations\b/i,
|
|
660
|
+
/\bart\.?\s*725\b/i,
|
|
661
|
+
/\bche-[0-9]{3}\.[0-9]{3}\.[0-9]{3}\b/i,
|
|
662
|
+
/\bchf\b/i,
|
|
663
|
+
/\b[0-9]{1,3}(?:'[0-9]{3})+\b/
|
|
664
|
+
];
|
|
665
|
+
return chPatterns.some((pattern) => pattern.test(lower));
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// src/finance/strategies/international.strategy.ts
|
|
670
|
+
var InternationalAccountingStrategy = class {
|
|
671
|
+
jurisdictionCode = "INTERNATIONAL";
|
|
672
|
+
standard = "OTHER";
|
|
673
|
+
defaultCurrency = "EUR";
|
|
674
|
+
cleanNumber(value) {
|
|
675
|
+
if (typeof value === "number") {
|
|
676
|
+
return Number.isFinite(value) ? value : 0;
|
|
677
|
+
}
|
|
678
|
+
if (!value || typeof value !== "string") {
|
|
679
|
+
return 0;
|
|
680
|
+
}
|
|
681
|
+
let str = value.trim();
|
|
682
|
+
let isNegative = false;
|
|
683
|
+
const parenMatch = str.match(/^[(\[]\s*(.+?)\s*[)\]]$/);
|
|
684
|
+
if (parenMatch) {
|
|
685
|
+
isNegative = true;
|
|
686
|
+
str = parenMatch[1];
|
|
687
|
+
} else if (str.startsWith("-")) {
|
|
688
|
+
isNegative = true;
|
|
689
|
+
str = str.substring(1).trim();
|
|
690
|
+
}
|
|
691
|
+
let multiplier = 1;
|
|
692
|
+
if (/[kK]\b/.test(str)) {
|
|
693
|
+
multiplier = 1e3;
|
|
694
|
+
str = str.replace(/[kK]\b/g, "").trim();
|
|
695
|
+
} else if (/[mM]\b/.test(str)) {
|
|
696
|
+
multiplier = 1e6;
|
|
697
|
+
str = str.replace(/[mM]\b/g, "").trim();
|
|
698
|
+
}
|
|
699
|
+
str = str.replace(/[€$£]/g, "").replace(/\b(?:eur|usd|gbp|chf)\b/gi, "").trim();
|
|
700
|
+
if (str.includes(",") && str.includes(".")) {
|
|
701
|
+
if (str.lastIndexOf(",") > str.lastIndexOf(".")) {
|
|
702
|
+
str = str.replace(/\./g, "").replace(",", ".");
|
|
703
|
+
} else {
|
|
704
|
+
str = str.replace(/,/g, "");
|
|
705
|
+
}
|
|
706
|
+
} else if (str.includes(",")) {
|
|
707
|
+
if (/,\d{1,2}$/.test(str)) {
|
|
708
|
+
str = str.replace(",", ".");
|
|
709
|
+
} else {
|
|
710
|
+
str = str.replace(",", "");
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
str = str.replace(/\s+/g, "");
|
|
714
|
+
const parsed = parseFloat(str);
|
|
715
|
+
if (isNaN(parsed)) {
|
|
716
|
+
return 0;
|
|
717
|
+
}
|
|
718
|
+
const finalVal = parsed * multiplier;
|
|
719
|
+
return isNegative ? -Math.abs(finalVal) : finalVal;
|
|
720
|
+
}
|
|
721
|
+
cleanJSON(rawText) {
|
|
722
|
+
let result = rawText;
|
|
723
|
+
result = result.replace(
|
|
724
|
+
/(:\s*)[(\[]\s*([0-9][0-9,.\s]*(?:k|m|€|\$|£)?)\s*[)\]]/gi,
|
|
725
|
+
(_, prefix, amountStr) => {
|
|
726
|
+
const num = this.cleanNumber(`(${amountStr})`);
|
|
727
|
+
return `${prefix}${num}`;
|
|
728
|
+
}
|
|
729
|
+
);
|
|
730
|
+
result = result.replace(
|
|
731
|
+
/(:\s*)"\s*[(\[]\s*([0-9][0-9,.\s]*(?:k|m|€|\$|£)?)\s*[)\]]\s*"/gi,
|
|
732
|
+
(_, prefix, amountStr) => {
|
|
733
|
+
const num = this.cleanNumber(`(${amountStr})`);
|
|
734
|
+
return `${prefix}${num}`;
|
|
735
|
+
}
|
|
736
|
+
);
|
|
737
|
+
return result;
|
|
738
|
+
}
|
|
739
|
+
detect(_text) {
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
// src/finance/factory.ts
|
|
745
|
+
var AccountingFactory = class {
|
|
746
|
+
static strategies = /* @__PURE__ */ new Map();
|
|
747
|
+
static detectionOrder = [];
|
|
748
|
+
static {
|
|
749
|
+
this.initDefaults();
|
|
750
|
+
}
|
|
751
|
+
static initDefaults() {
|
|
752
|
+
const fr = new FrenchPCGStrategy();
|
|
753
|
+
const us = new UsGAAPStrategy();
|
|
754
|
+
const uk = new UkIFRSStrategy();
|
|
755
|
+
const ch = new SwissCOStrategy();
|
|
756
|
+
const intl = new InternationalAccountingStrategy();
|
|
757
|
+
this.registerStrategy(ch);
|
|
758
|
+
this.registerStrategy(uk);
|
|
759
|
+
this.registerStrategy(us);
|
|
760
|
+
this.registerStrategy(fr);
|
|
761
|
+
this.registerStrategy(intl);
|
|
762
|
+
}
|
|
763
|
+
static registerStrategy(strategy) {
|
|
764
|
+
this.strategies.set(strategy.jurisdictionCode, strategy);
|
|
765
|
+
this.detectionOrder = [
|
|
766
|
+
strategy,
|
|
767
|
+
...this.detectionOrder.filter((s) => s.jurisdictionCode !== strategy.jurisdictionCode)
|
|
768
|
+
];
|
|
769
|
+
}
|
|
770
|
+
static getStrategy(code) {
|
|
771
|
+
if (!code) {
|
|
772
|
+
return this.strategies.get("FR") || new FrenchPCGStrategy();
|
|
773
|
+
}
|
|
774
|
+
const strategy = this.strategies.get(code);
|
|
775
|
+
if (!strategy) {
|
|
776
|
+
return this.strategies.get("FR") || new FrenchPCGStrategy();
|
|
777
|
+
}
|
|
778
|
+
return strategy;
|
|
779
|
+
}
|
|
780
|
+
static detectStrategy(documentText) {
|
|
781
|
+
for (const strategy of this.detectionOrder) {
|
|
782
|
+
if (strategy.jurisdictionCode !== "INTERNATIONAL" && strategy.detect(documentText)) {
|
|
783
|
+
return strategy;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return this.getStrategy("FR");
|
|
787
|
+
}
|
|
788
|
+
static resetDefaults() {
|
|
789
|
+
this.strategies.clear();
|
|
790
|
+
this.detectionOrder = [];
|
|
791
|
+
this.initDefaults();
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
// src/finance/normalizer.ts
|
|
796
|
+
function cleanFinancialJSON(rawText, options) {
|
|
797
|
+
if (!rawText) {
|
|
798
|
+
return "";
|
|
799
|
+
}
|
|
800
|
+
const jsonBlock = extractAndCleanJSON(rawText);
|
|
801
|
+
const strategy = options?.jurisdiction ? AccountingFactory.getStrategy(options.jurisdiction) : options?.autoDetect !== false ? AccountingFactory.detectStrategy(rawText) : AccountingFactory.getStrategy("FR");
|
|
802
|
+
return strategy.cleanJSON(jsonBlock);
|
|
803
|
+
}
|
|
804
|
+
|
|
141
805
|
// src/response-validator.ts
|
|
142
806
|
function extractAndCleanJSON(rawText) {
|
|
143
807
|
let cleaned = rawText.trim();
|
|
@@ -164,23 +828,161 @@ function extractAndCleanJSON(rawText) {
|
|
|
164
828
|
}
|
|
165
829
|
return cleaned;
|
|
166
830
|
}
|
|
167
|
-
function validateWithZod(rawText, schema) {
|
|
168
|
-
|
|
831
|
+
function validateWithZod(rawText, schema, options) {
|
|
832
|
+
let jsonString = extractAndCleanJSON(rawText);
|
|
833
|
+
if (options?.normalizer) {
|
|
834
|
+
jsonString = options.normalizer(jsonString);
|
|
835
|
+
} else if (options?.financialNormalizer) {
|
|
836
|
+
jsonString = cleanFinancialJSON(jsonString, { jurisdiction: options.jurisdiction });
|
|
837
|
+
}
|
|
169
838
|
let parsed;
|
|
170
839
|
try {
|
|
171
840
|
parsed = JSON.parse(jsonString);
|
|
172
|
-
} catch (
|
|
841
|
+
} catch (_error) {
|
|
173
842
|
const sanitized = jsonString.replace(/,\s*([}\]])/g, "$1").replace(/'/g, '"');
|
|
174
843
|
parsed = JSON.parse(sanitized);
|
|
175
844
|
}
|
|
176
845
|
return schema.parse(parsed);
|
|
177
846
|
}
|
|
178
847
|
|
|
848
|
+
// src/providers/http-client.ts
|
|
849
|
+
var DEFAULT_BASE_URLS = {
|
|
850
|
+
deepseek: "https://api.deepseek.com/v1",
|
|
851
|
+
mistral: "https://api.mistral.ai/v1",
|
|
852
|
+
openai: "https://api.openai.com/v1",
|
|
853
|
+
ollama: "http://localhost:11434/v1",
|
|
854
|
+
openrouter: "https://openrouter.ai/api/v1"
|
|
855
|
+
};
|
|
856
|
+
var HttpProviderClient = class {
|
|
857
|
+
name;
|
|
858
|
+
baseUrl;
|
|
859
|
+
apiKey;
|
|
860
|
+
providerType;
|
|
861
|
+
constructor(config) {
|
|
862
|
+
this.providerType = config.provider;
|
|
863
|
+
this.name = `${config.provider}-http`;
|
|
864
|
+
this.apiKey = config.apiKey;
|
|
865
|
+
this.baseUrl = config.baseUrl || DEFAULT_BASE_URLS[config.provider] || "https://api.openai.com/v1";
|
|
866
|
+
}
|
|
867
|
+
resolveEndpoint() {
|
|
868
|
+
const trimmed = this.baseUrl.replace(/\/+$/, "");
|
|
869
|
+
if (trimmed.endsWith("/chat/completions")) {
|
|
870
|
+
return trimmed;
|
|
871
|
+
}
|
|
872
|
+
return `${trimmed}/chat/completions`;
|
|
873
|
+
}
|
|
874
|
+
buildHeaders() {
|
|
875
|
+
const headers = {
|
|
876
|
+
"Content-Type": "application/json"
|
|
877
|
+
};
|
|
878
|
+
if (this.apiKey) {
|
|
879
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
880
|
+
}
|
|
881
|
+
if (this.providerType === "openrouter") {
|
|
882
|
+
headers["HTTP-Referer"] = "https://avantgate.dev";
|
|
883
|
+
headers["X-Title"] = "AvantGate";
|
|
884
|
+
}
|
|
885
|
+
return headers;
|
|
886
|
+
}
|
|
887
|
+
buildPayload(options, model) {
|
|
888
|
+
return {
|
|
889
|
+
model,
|
|
890
|
+
messages: options.messages.map((m) => ({
|
|
891
|
+
role: m.role,
|
|
892
|
+
content: m.content
|
|
893
|
+
})),
|
|
894
|
+
temperature: options.temperature ?? 0.2,
|
|
895
|
+
...options.responseFormat ? { response_format: options.responseFormat } : {}
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
extractUsage(usageData) {
|
|
899
|
+
if (!usageData) {
|
|
900
|
+
return void 0;
|
|
901
|
+
}
|
|
902
|
+
return {
|
|
903
|
+
promptTokens: usageData.prompt_tokens,
|
|
904
|
+
completionTokens: usageData.completion_tokens,
|
|
905
|
+
totalTokens: usageData.total_tokens,
|
|
906
|
+
promptCacheHitTokens: usageData.prompt_tokens_details?.cached_tokens
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
async complete(options) {
|
|
910
|
+
const endpoint = this.resolveEndpoint();
|
|
911
|
+
const model = options.model ?? "default";
|
|
912
|
+
const headers = this.buildHeaders();
|
|
913
|
+
const body = JSON.stringify(this.buildPayload(options, model));
|
|
914
|
+
const response = await fetch(endpoint, {
|
|
915
|
+
method: "POST",
|
|
916
|
+
headers,
|
|
917
|
+
body
|
|
918
|
+
});
|
|
919
|
+
if (!response.ok) {
|
|
920
|
+
const errorText = await response.text();
|
|
921
|
+
throw new Error(
|
|
922
|
+
`[AvantGate HTTP Provider ${this.providerType}] HTTP ${response.status} ${response.statusText}: ${errorText}`
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
const data = await response.json();
|
|
926
|
+
const text = data.choices?.[0]?.message?.content ?? "";
|
|
927
|
+
const usage = this.extractUsage(data.usage);
|
|
928
|
+
return { text, usage };
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
function createHttpProviderClient(config) {
|
|
932
|
+
return new HttpProviderClient(config);
|
|
933
|
+
}
|
|
934
|
+
|
|
179
935
|
// src/control-layer.ts
|
|
180
936
|
var AvantGateControlLayer = class {
|
|
181
937
|
config;
|
|
938
|
+
cachedPricingAdapter;
|
|
182
939
|
constructor(config) {
|
|
183
|
-
this.config =
|
|
940
|
+
this.config = {
|
|
941
|
+
...config,
|
|
942
|
+
primary: this.resolveProviderConfig(config.primary),
|
|
943
|
+
fallback: this.resolveProviderConfig(config.fallback),
|
|
944
|
+
emergencyFallback: this.resolveProviderConfig(config.emergencyFallback)
|
|
945
|
+
};
|
|
946
|
+
if (config.pricingAdapter) {
|
|
947
|
+
this.cachedPricingAdapter = new CachedPricingAdapter(
|
|
948
|
+
config.pricingAdapter,
|
|
949
|
+
config.pricingCacheTtlMs ?? 5 * 60 * 1e3
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
resolveProviderConfig(provider) {
|
|
954
|
+
if (!provider) return void 0;
|
|
955
|
+
if (provider.client) return provider;
|
|
956
|
+
if (provider.apiKey || provider.baseUrl || provider.provider === "ollama") {
|
|
957
|
+
return {
|
|
958
|
+
...provider,
|
|
959
|
+
client: createHttpProviderClient(provider)
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
return provider;
|
|
963
|
+
}
|
|
964
|
+
findProviderConfig(provider, model) {
|
|
965
|
+
const list = [this.config.primary, this.config.fallback, this.config.emergencyFallback].filter(
|
|
966
|
+
(p) => Boolean(p)
|
|
967
|
+
);
|
|
968
|
+
if (provider) {
|
|
969
|
+
const matchProvider = list.find((p) => p.provider === provider);
|
|
970
|
+
if (matchProvider) return matchProvider;
|
|
971
|
+
}
|
|
972
|
+
if (model) {
|
|
973
|
+
const matchModel = list.find((p) => p.model === model);
|
|
974
|
+
if (matchModel) return matchModel;
|
|
975
|
+
}
|
|
976
|
+
return this.config.primary;
|
|
977
|
+
}
|
|
978
|
+
calculateCost(model, promptTokens, completionTokens, cacheHitTokens = 0, provider) {
|
|
979
|
+
const providerCfg = this.findProviderConfig(provider, model);
|
|
980
|
+
return calculateCostUSD(model, promptTokens, completionTokens, cacheHitTokens, {
|
|
981
|
+
provider: provider ?? providerCfg?.provider,
|
|
982
|
+
providerPricing: providerCfg?.pricing,
|
|
983
|
+
customPricing: this.config.customPricing,
|
|
984
|
+
adapter: this.cachedPricingAdapter
|
|
985
|
+
});
|
|
184
986
|
}
|
|
185
987
|
applySecurityGuards(userQuery) {
|
|
186
988
|
const guard = validateUserInput(userQuery, {
|
|
@@ -195,6 +997,46 @@ var AvantGateControlLayer = class {
|
|
|
195
997
|
}
|
|
196
998
|
return userQuery;
|
|
197
999
|
}
|
|
1000
|
+
checkPreflightBudget(estimatedPromptTokens, targetModel, provider) {
|
|
1001
|
+
if (this.config.maxTokenBudget !== void 0 && estimatedPromptTokens > this.config.maxTokenBudget) {
|
|
1002
|
+
throw new BudgetExceededError(
|
|
1003
|
+
`[AvantGate Budget Guard] Pre-flight token budget exceeded: estimated prompt (${estimatedPromptTokens} tokens) exceeds maxTokenBudget (${this.config.maxTokenBudget}).`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
if (this.config.maxCostUSD !== void 0) {
|
|
1007
|
+
const isLocalFree = provider === "ollama" || targetModel.toLowerCase().includes("ollama");
|
|
1008
|
+
const providerCfg = this.findProviderConfig(provider, targetModel);
|
|
1009
|
+
const price = resolveModelPrice(targetModel, {
|
|
1010
|
+
provider: provider ?? providerCfg?.provider,
|
|
1011
|
+
providerPricing: providerCfg?.pricing,
|
|
1012
|
+
customPricing: this.config.customPricing,
|
|
1013
|
+
adapter: this.cachedPricingAdapter
|
|
1014
|
+
});
|
|
1015
|
+
if (!isLocalFree && !price) {
|
|
1016
|
+
throw new ConfigurationError(
|
|
1017
|
+
`[AvantGate Configuration Error] 'maxCostUSD' was set to $${this.config.maxCostUSD}, but no pricing was configured for model '${targetModel}'. Please define pricing in ProviderConfig, customPricing, or via PricingAdapter.`
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
const estimatedPromptCost = this.calculateCost(targetModel, estimatedPromptTokens, 0, 0, provider);
|
|
1021
|
+
if (estimatedPromptCost > this.config.maxCostUSD) {
|
|
1022
|
+
throw new BudgetExceededError(
|
|
1023
|
+
`[AvantGate Budget Guard] Pre-flight cost budget exceeded: estimated prompt cost ($${estimatedPromptCost.toFixed(6)}) exceeds maxCostUSD ($${this.config.maxCostUSD}).`
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
checkPostExecutionBudget(tokensTotal, costUSD) {
|
|
1029
|
+
if (this.config.maxTokenBudget !== void 0 && tokensTotal > this.config.maxTokenBudget) {
|
|
1030
|
+
throw new BudgetExceededError(
|
|
1031
|
+
`[AvantGate Budget Guard] Execution total tokens (${tokensTotal}) exceeded maxTokenBudget (${this.config.maxTokenBudget}).`
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
if (this.config.maxCostUSD !== void 0 && costUSD > this.config.maxCostUSD) {
|
|
1035
|
+
throw new BudgetExceededError(
|
|
1036
|
+
`[AvantGate Budget Guard] Execution cost ($${costUSD.toFixed(6)}) exceeded maxCostUSD ($${this.config.maxCostUSD}).`
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
198
1040
|
buildMessages(systemPrompt, query) {
|
|
199
1041
|
const messages = [];
|
|
200
1042
|
if (systemPrompt) {
|
|
@@ -207,7 +1049,12 @@ var AvantGateControlLayer = class {
|
|
|
207
1049
|
const promptTokens = rawUsage?.promptTokens ?? Math.ceil(query.length / 4);
|
|
208
1050
|
const completionTokens = rawUsage?.completionTokens ?? Math.ceil(text.length / 4);
|
|
209
1051
|
const totalTokens = rawUsage?.totalTokens ?? promptTokens + completionTokens;
|
|
210
|
-
return {
|
|
1052
|
+
return {
|
|
1053
|
+
promptTokens,
|
|
1054
|
+
completionTokens,
|
|
1055
|
+
totalTokens,
|
|
1056
|
+
promptCacheHitTokens: rawUsage?.promptCacheHitTokens
|
|
1057
|
+
};
|
|
211
1058
|
}
|
|
212
1059
|
getProviderChain() {
|
|
213
1060
|
return [
|
|
@@ -219,7 +1066,7 @@ var AvantGateControlLayer = class {
|
|
|
219
1066
|
executeSimulation(query, systemPrompt) {
|
|
220
1067
|
const promptTokens = Math.ceil(((systemPrompt?.length ?? 0) + query.length) / 4);
|
|
221
1068
|
const completionTokens = 50;
|
|
222
|
-
const cost =
|
|
1069
|
+
const cost = this.calculateCost(this.config.primary.model, promptTokens, completionTokens);
|
|
223
1070
|
return {
|
|
224
1071
|
text: `[AvantGate In-Process Engine] Response simulation for model: ${this.config.primary.model}`,
|
|
225
1072
|
tokens: {
|
|
@@ -248,14 +1095,15 @@ var AvantGateControlLayer = class {
|
|
|
248
1095
|
attempts: 1
|
|
249
1096
|
};
|
|
250
1097
|
}
|
|
251
|
-
async executeProviderPipeline(messages, query, temperature) {
|
|
1098
|
+
async executeProviderPipeline(messages, query, temperature, modelOverride) {
|
|
252
1099
|
const chain = this.getProviderChain();
|
|
253
1100
|
let lastError;
|
|
254
1101
|
for (let index = 0; index < chain.length; index++) {
|
|
255
1102
|
const providerConfig = chain[index];
|
|
1103
|
+
const targetModel = index === 0 && modelOverride ? modelOverride : providerConfig.model;
|
|
256
1104
|
try {
|
|
257
1105
|
const response = await providerConfig.client.complete({
|
|
258
|
-
model:
|
|
1106
|
+
model: targetModel,
|
|
259
1107
|
messages,
|
|
260
1108
|
temperature: temperature ?? 0.2
|
|
261
1109
|
});
|
|
@@ -263,7 +1111,7 @@ var AvantGateControlLayer = class {
|
|
|
263
1111
|
return {
|
|
264
1112
|
responseText: response.text,
|
|
265
1113
|
usage,
|
|
266
|
-
modelUsed:
|
|
1114
|
+
modelUsed: targetModel,
|
|
267
1115
|
failoverOccurred: index > 0,
|
|
268
1116
|
attempts: index + 1
|
|
269
1117
|
};
|
|
@@ -274,10 +1122,11 @@ var AvantGateControlLayer = class {
|
|
|
274
1122
|
throw lastError;
|
|
275
1123
|
}
|
|
276
1124
|
assembleResult(output) {
|
|
277
|
-
const costUSD =
|
|
1125
|
+
const costUSD = this.calculateCost(
|
|
278
1126
|
output.modelUsed,
|
|
279
1127
|
output.usage.promptTokens,
|
|
280
|
-
output.usage.completionTokens
|
|
1128
|
+
output.usage.completionTokens,
|
|
1129
|
+
output.usage.promptCacheHitTokens ?? 0
|
|
281
1130
|
);
|
|
282
1131
|
return {
|
|
283
1132
|
text: output.responseText,
|
|
@@ -306,18 +1155,27 @@ var AvantGateControlLayer = class {
|
|
|
306
1155
|
});
|
|
307
1156
|
}
|
|
308
1157
|
/**
|
|
309
|
-
* Exécute une requête avec garde d'entrée, masquage PII, et calcul des coûts.
|
|
1158
|
+
* Exécute une requête avec garde d'entrée, masquage PII, garde pré-vol et calcul des coûts.
|
|
310
1159
|
*/
|
|
311
1160
|
async execute(options) {
|
|
312
1161
|
const sanitizedQuery = this.applySecurityGuards(options.userQuery);
|
|
313
1162
|
const messages = this.buildMessages(options.systemPrompt, sanitizedQuery);
|
|
1163
|
+
const promptLength = (options.systemPrompt?.length ?? 0) + sanitizedQuery.length;
|
|
1164
|
+
const estimatedPromptTokens = Math.ceil(promptLength / 4);
|
|
1165
|
+
this.checkPreflightBudget(estimatedPromptTokens, this.config.primary.model, this.config.primary.provider);
|
|
314
1166
|
if (!options.providerOverride && this.getProviderChain().length === 0) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
1167
|
+
if (this.config.mockSimulation) {
|
|
1168
|
+
const simResult = this.executeSimulation(sanitizedQuery, options.systemPrompt);
|
|
1169
|
+
await this.notifyAuditSink(simResult);
|
|
1170
|
+
return simResult;
|
|
1171
|
+
}
|
|
1172
|
+
throw new ConfigurationError(
|
|
1173
|
+
"[AvantGate Configuration Error] No active LLM provider configured. Provide a client implementing LLMProviderPort or configure credentials (apiKey / baseUrl)."
|
|
1174
|
+
);
|
|
318
1175
|
}
|
|
319
1176
|
const output = options.providerOverride ? await this.executeOverrideProvider(options.providerOverride, messages, sanitizedQuery, options.temperature) : await this.executeProviderPipeline(messages, sanitizedQuery, options.temperature);
|
|
320
1177
|
const result = this.assembleResult(output);
|
|
1178
|
+
this.checkPostExecutionBudget(result.tokens.total, result.costUSD);
|
|
321
1179
|
await this.notifyAuditSink(result);
|
|
322
1180
|
return result;
|
|
323
1181
|
}
|
|
@@ -331,7 +1189,13 @@ var AvantGateControlLayer = class {
|
|
|
331
1189
|
temperature: options.temperature ?? 0.1,
|
|
332
1190
|
providerOverride: options.providerOverride
|
|
333
1191
|
});
|
|
334
|
-
const
|
|
1192
|
+
const isFinancial = Boolean(
|
|
1193
|
+
this.config.features?.finance?.enableFrenchAccounting || this.config.features?.finance
|
|
1194
|
+
);
|
|
1195
|
+
const parsedData = validateWithZod(rawResult.text, options.schema, {
|
|
1196
|
+
financialNormalizer: isFinancial,
|
|
1197
|
+
jurisdiction: this.config.features?.finance?.jurisdiction
|
|
1198
|
+
});
|
|
335
1199
|
return {
|
|
336
1200
|
data: parsedData,
|
|
337
1201
|
rawText: rawResult.text,
|
|
@@ -341,20 +1205,453 @@ var AvantGateControlLayer = class {
|
|
|
341
1205
|
failoverOccurred: rawResult.failoverOccurred
|
|
342
1206
|
};
|
|
343
1207
|
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Méthode unifiée de premier niveau pour l'extraction structurée sans code boilerplate.
|
|
1210
|
+
* Gère le failover multi-fournisseurs, les retries, la validation Zod et la normalisation financière.
|
|
1211
|
+
*/
|
|
1212
|
+
async generateStructuredOutput(options) {
|
|
1213
|
+
const maxRetries = options.maxRetries ?? this.config.retryOptions?.maxRetries ?? 2;
|
|
1214
|
+
const modelToUse = options.model ?? this.config.primary.model;
|
|
1215
|
+
const processedMessages = options.messages.map((msg) => {
|
|
1216
|
+
if (msg.role === "user") {
|
|
1217
|
+
return { ...msg, content: this.applySecurityGuards(msg.content) };
|
|
1218
|
+
}
|
|
1219
|
+
return msg;
|
|
1220
|
+
});
|
|
1221
|
+
const promptLength = processedMessages.reduce((sum, msg) => sum + msg.content.length, 0);
|
|
1222
|
+
const estimatedPromptTokens = Math.ceil(promptLength / 4);
|
|
1223
|
+
this.checkPreflightBudget(estimatedPromptTokens, modelToUse);
|
|
1224
|
+
let lastError;
|
|
1225
|
+
let accumulatedPromptTokens = 0;
|
|
1226
|
+
let accumulatedCompletionTokens = 0;
|
|
1227
|
+
let failoverOccurred = false;
|
|
1228
|
+
let modelUsed = modelToUse;
|
|
1229
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1230
|
+
try {
|
|
1231
|
+
let responseText = "";
|
|
1232
|
+
let attemptPromptTokens = 0;
|
|
1233
|
+
let attemptCompletionTokens = 0;
|
|
1234
|
+
if (options.providerOverride) {
|
|
1235
|
+
const res = await options.providerOverride.complete({
|
|
1236
|
+
model: modelToUse,
|
|
1237
|
+
messages: processedMessages,
|
|
1238
|
+
temperature: options.temperature ?? 0.1
|
|
1239
|
+
});
|
|
1240
|
+
responseText = res.text;
|
|
1241
|
+
const usage = this.resolveTokens(res.usage, JSON.stringify(processedMessages), responseText);
|
|
1242
|
+
attemptPromptTokens = usage.promptTokens;
|
|
1243
|
+
attemptCompletionTokens = usage.completionTokens;
|
|
1244
|
+
modelUsed = modelToUse;
|
|
1245
|
+
} else if (this.getProviderChain().length === 0) {
|
|
1246
|
+
if (this.config.mockSimulation) {
|
|
1247
|
+
const sim = this.executeSimulation(JSON.stringify(processedMessages));
|
|
1248
|
+
responseText = sim.text;
|
|
1249
|
+
attemptPromptTokens = sim.tokens.prompt;
|
|
1250
|
+
attemptCompletionTokens = sim.tokens.completion;
|
|
1251
|
+
} else {
|
|
1252
|
+
throw new ConfigurationError(
|
|
1253
|
+
"[AvantGate Configuration Error] No active LLM provider configured. Provide a client implementing LLMProviderPort or configure credentials (apiKey / baseUrl)."
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
} else {
|
|
1257
|
+
const output = await this.executeProviderPipeline(
|
|
1258
|
+
processedMessages,
|
|
1259
|
+
JSON.stringify(processedMessages),
|
|
1260
|
+
options.temperature ?? 0.1,
|
|
1261
|
+
options.model
|
|
1262
|
+
);
|
|
1263
|
+
responseText = output.responseText;
|
|
1264
|
+
attemptPromptTokens = output.usage.promptTokens;
|
|
1265
|
+
attemptCompletionTokens = output.usage.completionTokens;
|
|
1266
|
+
modelUsed = output.modelUsed;
|
|
1267
|
+
failoverOccurred = output.failoverOccurred;
|
|
1268
|
+
}
|
|
1269
|
+
accumulatedPromptTokens += attemptPromptTokens;
|
|
1270
|
+
accumulatedCompletionTokens += attemptCompletionTokens;
|
|
1271
|
+
const isFinancial = options.financialNormalizer ?? Boolean(
|
|
1272
|
+
this.config.features?.finance?.enableFrenchAccounting || this.config.features?.finance
|
|
1273
|
+
);
|
|
1274
|
+
const parsedData = validateWithZod(responseText, options.schema, {
|
|
1275
|
+
financialNormalizer: isFinancial,
|
|
1276
|
+
jurisdiction: this.config.features?.finance?.jurisdiction
|
|
1277
|
+
});
|
|
1278
|
+
const totalTokens = accumulatedPromptTokens + accumulatedCompletionTokens;
|
|
1279
|
+
const costUSD = this.calculateCost(modelUsed, accumulatedPromptTokens, accumulatedCompletionTokens);
|
|
1280
|
+
this.checkPostExecutionBudget(totalTokens, costUSD);
|
|
1281
|
+
const result = {
|
|
1282
|
+
data: parsedData,
|
|
1283
|
+
rawText: responseText,
|
|
1284
|
+
tokens: {
|
|
1285
|
+
prompt: accumulatedPromptTokens,
|
|
1286
|
+
completion: accumulatedCompletionTokens,
|
|
1287
|
+
total: totalTokens
|
|
1288
|
+
},
|
|
1289
|
+
costUSD,
|
|
1290
|
+
modelUsed,
|
|
1291
|
+
failoverOccurred
|
|
1292
|
+
};
|
|
1293
|
+
await this.notifyAuditSink({
|
|
1294
|
+
text: responseText,
|
|
1295
|
+
tokens: result.tokens,
|
|
1296
|
+
costUSD: result.costUSD,
|
|
1297
|
+
modelUsed: result.modelUsed,
|
|
1298
|
+
failoverOccurred: result.failoverOccurred,
|
|
1299
|
+
attempts: attempt + 1
|
|
1300
|
+
});
|
|
1301
|
+
return result;
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
lastError = err;
|
|
1304
|
+
if (attempt < maxRetries) {
|
|
1305
|
+
const delay = (this.config.retryOptions?.initialDelayMs ?? 200) * Math.pow(this.config.retryOptions?.backoffFactor ?? 1.5, attempt);
|
|
1306
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
throw lastError;
|
|
1311
|
+
}
|
|
344
1312
|
};
|
|
345
1313
|
function createLLMControlLayer(config) {
|
|
346
1314
|
return new AvantGateControlLayer(config);
|
|
347
1315
|
}
|
|
348
1316
|
var createAvantGate = createLLMControlLayer;
|
|
1317
|
+
|
|
1318
|
+
// src/prompts/prompt-template.ts
|
|
1319
|
+
var DEFAULT_JAILBREAK_PATTERNS = [
|
|
1320
|
+
/ignore\s+(all\s+)?(previous|prior)\s+(instructions|rules)/i,
|
|
1321
|
+
/disregard\s+(all\s+)?(previous|prior)\s+(instructions|rules)/i,
|
|
1322
|
+
/system\s+override/i,
|
|
1323
|
+
/you\s+are\s+now\s+(unrestricted|DAN|jailbroken)/i,
|
|
1324
|
+
/<\s*\|\s*im_start\s*\|>/i,
|
|
1325
|
+
/\[SYSTEM_PROMPT\]/i
|
|
1326
|
+
];
|
|
1327
|
+
var PromptTemplate = class {
|
|
1328
|
+
id;
|
|
1329
|
+
version;
|
|
1330
|
+
label;
|
|
1331
|
+
description;
|
|
1332
|
+
template;
|
|
1333
|
+
inputSchema;
|
|
1334
|
+
blockPatterns;
|
|
1335
|
+
antiInjectionEnabled;
|
|
1336
|
+
customSanitizer;
|
|
1337
|
+
constructor(options) {
|
|
1338
|
+
this.id = options.id;
|
|
1339
|
+
this.version = options.version;
|
|
1340
|
+
this.label = options.label || "production";
|
|
1341
|
+
this.description = options.description;
|
|
1342
|
+
this.template = options.template;
|
|
1343
|
+
this.inputSchema = options.inputSchema;
|
|
1344
|
+
this.antiInjectionEnabled = options.antiInjection?.enabled !== false;
|
|
1345
|
+
this.blockPatterns = options.antiInjection?.blockPatterns || DEFAULT_JAILBREAK_PATTERNS;
|
|
1346
|
+
this.customSanitizer = options.antiInjection?.sanitizer;
|
|
1347
|
+
}
|
|
1348
|
+
validateUserInput(variables) {
|
|
1349
|
+
const threats = [];
|
|
1350
|
+
if (this.inputSchema) {
|
|
1351
|
+
const parsed = this.inputSchema.safeParse(variables);
|
|
1352
|
+
if (!parsed.success) {
|
|
1353
|
+
return {
|
|
1354
|
+
isValid: false,
|
|
1355
|
+
threats: parsed.error.issues.map(
|
|
1356
|
+
(issue) => `${issue.path.join(".")}: ${issue.message}`
|
|
1357
|
+
)
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (this.antiInjectionEnabled) {
|
|
1362
|
+
for (const [key, val] of Object.entries(variables)) {
|
|
1363
|
+
if (typeof val === "string") {
|
|
1364
|
+
for (const pattern of this.blockPatterns) {
|
|
1365
|
+
if (pattern.test(val)) {
|
|
1366
|
+
threats.push(
|
|
1367
|
+
`Potential prompt injection in variable "${key}" matching pattern ${pattern}`
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return { isValid: threats.length === 0, threats };
|
|
1375
|
+
}
|
|
1376
|
+
format(variables) {
|
|
1377
|
+
const check = this.validateUserInput(variables);
|
|
1378
|
+
if (!check.isValid) {
|
|
1379
|
+
throw new Error(
|
|
1380
|
+
`[PromptTemplate:${this.id}] Validation failed: ${check.threats.join(", ")}`
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
const interpolated = this.template.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (_, key) => {
|
|
1384
|
+
let value = variables[key];
|
|
1385
|
+
if (value === void 0 || value === null) {
|
|
1386
|
+
return "";
|
|
1387
|
+
}
|
|
1388
|
+
let stringValue = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
1389
|
+
if (this.customSanitizer) {
|
|
1390
|
+
stringValue = this.customSanitizer(stringValue);
|
|
1391
|
+
}
|
|
1392
|
+
return stringValue;
|
|
1393
|
+
});
|
|
1394
|
+
return interpolated.split("\n").filter((line, index, arr) => line.trim() !== "" || index > 0 && arr[index - 1].trim() !== "").join("\n");
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
|
|
1398
|
+
// src/prompts/prompt-builder.ts
|
|
1399
|
+
var import_zod = require("zod");
|
|
1400
|
+
var PromptBuilder = class {
|
|
1401
|
+
personaSlot = "";
|
|
1402
|
+
rulesSlot = [];
|
|
1403
|
+
retryHintSlot = "";
|
|
1404
|
+
fewShotSlot = [];
|
|
1405
|
+
pinnedFactsSlot = "";
|
|
1406
|
+
contextSlot = "";
|
|
1407
|
+
userPayloadSlot = "";
|
|
1408
|
+
schemaContractText = "";
|
|
1409
|
+
constructor(templateOrId) {
|
|
1410
|
+
if (templateOrId instanceof PromptTemplate) {
|
|
1411
|
+
this.personaSlot = templateOrId.template;
|
|
1412
|
+
} else if (typeof templateOrId === "string") {
|
|
1413
|
+
this.personaSlot = templateOrId.trim();
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
withPersona(persona) {
|
|
1417
|
+
this.personaSlot = persona.trim();
|
|
1418
|
+
return this;
|
|
1419
|
+
}
|
|
1420
|
+
withRules(rules) {
|
|
1421
|
+
if (Array.isArray(rules)) {
|
|
1422
|
+
this.rulesSlot.push(...rules.map((rule) => rule.trim()).filter(Boolean));
|
|
1423
|
+
} else if (rules) {
|
|
1424
|
+
this.rulesSlot.push(rules.trim());
|
|
1425
|
+
}
|
|
1426
|
+
return this;
|
|
1427
|
+
}
|
|
1428
|
+
withRetryHint(hint) {
|
|
1429
|
+
this.retryHintSlot = hint.trim();
|
|
1430
|
+
return this;
|
|
1431
|
+
}
|
|
1432
|
+
withFewShot(examples) {
|
|
1433
|
+
this.fewShotSlot = examples;
|
|
1434
|
+
return this;
|
|
1435
|
+
}
|
|
1436
|
+
withPinnedFacts(facts) {
|
|
1437
|
+
if (!facts) return this;
|
|
1438
|
+
if (typeof facts === "string") {
|
|
1439
|
+
this.pinnedFactsSlot = facts.trim();
|
|
1440
|
+
return this;
|
|
1441
|
+
}
|
|
1442
|
+
this.pinnedFactsSlot = Object.entries(facts).filter(([, val]) => val !== void 0 && val !== null && val !== "").map(([key, val]) => `\u2022 ${key} : ${Array.isArray(val) ? val.join(", ") : String(val)}`).join("\n");
|
|
1443
|
+
return this;
|
|
1444
|
+
}
|
|
1445
|
+
withContext(context) {
|
|
1446
|
+
this.contextSlot = context.trim();
|
|
1447
|
+
return this;
|
|
1448
|
+
}
|
|
1449
|
+
withUserPayload(payload) {
|
|
1450
|
+
this.userPayloadSlot = payload.trim();
|
|
1451
|
+
return this;
|
|
1452
|
+
}
|
|
1453
|
+
schemaContract(schema, options) {
|
|
1454
|
+
let shapeDescription = "JSON Object";
|
|
1455
|
+
if (schema instanceof import_zod.z.ZodObject) {
|
|
1456
|
+
shapeDescription = JSON.stringify(Object.keys(schema.shape));
|
|
1457
|
+
}
|
|
1458
|
+
this.schemaContractText = [
|
|
1459
|
+
`[DIRECTIVE DE CONTRAT DE SORTIE JSON STRICT]`,
|
|
1460
|
+
`Tu DOIS renvoyer UNIQUEMENT un objet JSON valide, sans balises markdown (\`\`\`json), sans texte avant ou apr\xE8s.`,
|
|
1461
|
+
`Champs obligatoires et types attendus :`,
|
|
1462
|
+
options?.schemaName ? `Sch\xE9ma : ${options.schemaName}` : "",
|
|
1463
|
+
shapeDescription
|
|
1464
|
+
].filter(Boolean).join("\n");
|
|
1465
|
+
return this;
|
|
1466
|
+
}
|
|
1467
|
+
assembleMessages(pinnedFacts, context, userPayload) {
|
|
1468
|
+
const messages = [];
|
|
1469
|
+
let system0 = this.personaSlot || "Tu es un assistant expert.";
|
|
1470
|
+
if (this.schemaContractText) {
|
|
1471
|
+
system0 += `
|
|
1472
|
+
|
|
1473
|
+
${this.schemaContractText}`;
|
|
1474
|
+
}
|
|
1475
|
+
messages.push({ role: "system", content: system0 });
|
|
1476
|
+
const dynamicParts = [];
|
|
1477
|
+
if (this.rulesSlot.length > 0) {
|
|
1478
|
+
dynamicParts.push(
|
|
1479
|
+
`[R\xC8GLES ET CONSIGNES M\xC9TIER]
|
|
1480
|
+
${this.rulesSlot.map((rule, idx) => `${idx + 1}. ${rule}`).join("\n")}`
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
if (this.retryHintSlot) {
|
|
1484
|
+
dynamicParts.push(`[INSTRUCTION DE CORRECTION / RETRY]
|
|
1485
|
+
${this.retryHintSlot}`);
|
|
1486
|
+
}
|
|
1487
|
+
if (dynamicParts.length > 0) {
|
|
1488
|
+
messages.push({ role: "system", content: dynamicParts.join("\n\n") });
|
|
1489
|
+
}
|
|
1490
|
+
if (this.fewShotSlot.length > 0) {
|
|
1491
|
+
const examplesContent = this.fewShotSlot.map(
|
|
1492
|
+
(ex, idx) => `[Exemple ${idx + 1}]
|
|
1493
|
+
Question: ${ex.question}
|
|
1494
|
+
R\xE9ponse attendue: ${ex.answer}`
|
|
1495
|
+
).join("\n\n");
|
|
1496
|
+
messages.push({ role: "system", content: `[EXEMPLES DE R\xC9F\xC9RENCE]
|
|
1497
|
+
${examplesContent}` });
|
|
1498
|
+
}
|
|
1499
|
+
const userParts = [];
|
|
1500
|
+
if (pinnedFacts) {
|
|
1501
|
+
userParts.push(`[FAITS STRUCTUR\xC9S]
|
|
1502
|
+
${pinnedFacts}`);
|
|
1503
|
+
}
|
|
1504
|
+
if (context) {
|
|
1505
|
+
userParts.push(`[CONTEXTE DOCUMENTAIRE]
|
|
1506
|
+
${context}`);
|
|
1507
|
+
}
|
|
1508
|
+
if (userPayload) {
|
|
1509
|
+
const labelNeeded = Boolean(pinnedFacts || context);
|
|
1510
|
+
userParts.push(labelNeeded ? `[DONN\xC9ES \xC0 ANALYSER]
|
|
1511
|
+
${userPayload}` : userPayload);
|
|
1512
|
+
}
|
|
1513
|
+
messages.push({ role: "user", content: userParts.join("\n\n") });
|
|
1514
|
+
return messages;
|
|
1515
|
+
}
|
|
1516
|
+
toMessages() {
|
|
1517
|
+
return this.assembleMessages(this.pinnedFactsSlot, this.contextSlot, this.userPayloadSlot);
|
|
1518
|
+
}
|
|
1519
|
+
build(budget) {
|
|
1520
|
+
if (!budget) {
|
|
1521
|
+
const messages2 = this.toMessages();
|
|
1522
|
+
const promptText2 = messages2.map((msg) => `${msg.role.toUpperCase()}:
|
|
1523
|
+
${msg.content}`).join("\n\n");
|
|
1524
|
+
return {
|
|
1525
|
+
messages: messages2,
|
|
1526
|
+
promptText: promptText2,
|
|
1527
|
+
allocatedTokens: Math.ceil(promptText2.length / 4),
|
|
1528
|
+
isTruncated: false
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
budget.reset();
|
|
1532
|
+
let isTruncated = false;
|
|
1533
|
+
let truncatedSlot;
|
|
1534
|
+
let system0 = this.personaSlot || "Tu es un assistant expert.";
|
|
1535
|
+
if (this.schemaContractText) {
|
|
1536
|
+
system0 += `
|
|
1537
|
+
|
|
1538
|
+
${this.schemaContractText}`;
|
|
1539
|
+
}
|
|
1540
|
+
budget.forceReserve("persona", system0);
|
|
1541
|
+
if (this.rulesSlot.length > 0 || this.retryHintSlot) {
|
|
1542
|
+
const rulesText = this.rulesSlot.map((rule, idx) => `${idx + 1}. ${rule}`).join("\n");
|
|
1543
|
+
budget.forceReserve("rules", `${rulesText}
|
|
1544
|
+
${this.retryHintSlot}`);
|
|
1545
|
+
}
|
|
1546
|
+
let payloadToUse = this.userPayloadSlot;
|
|
1547
|
+
let factsToUse = this.pinnedFactsSlot;
|
|
1548
|
+
let contextToUse = this.contextSlot;
|
|
1549
|
+
if (payloadToUse) {
|
|
1550
|
+
const payloadCost = budget.count(payloadToUse);
|
|
1551
|
+
if (payloadCost > budget.remaining()) {
|
|
1552
|
+
const charLimit = Math.max(100, budget.remaining() * 4);
|
|
1553
|
+
payloadToUse = payloadToUse.slice(0, charLimit);
|
|
1554
|
+
isTruncated = true;
|
|
1555
|
+
truncatedSlot = "user";
|
|
1556
|
+
}
|
|
1557
|
+
budget.reserve("user", payloadToUse);
|
|
1558
|
+
}
|
|
1559
|
+
if (factsToUse) {
|
|
1560
|
+
const factsCost = budget.count(factsToUse);
|
|
1561
|
+
if (factsCost > budget.remaining()) {
|
|
1562
|
+
const charLimit = Math.max(50, budget.remaining() * 4);
|
|
1563
|
+
factsToUse = factsToUse.slice(0, charLimit);
|
|
1564
|
+
isTruncated = true;
|
|
1565
|
+
truncatedSlot = "pinnedFacts";
|
|
1566
|
+
}
|
|
1567
|
+
budget.reserve("pinnedFacts", factsToUse);
|
|
1568
|
+
}
|
|
1569
|
+
if (contextToUse) {
|
|
1570
|
+
const contextCost = budget.count(contextToUse);
|
|
1571
|
+
if (contextCost > budget.remaining()) {
|
|
1572
|
+
const charLimit = Math.max(0, budget.remaining() * 4);
|
|
1573
|
+
contextToUse = contextToUse.slice(0, charLimit);
|
|
1574
|
+
isTruncated = true;
|
|
1575
|
+
truncatedSlot = "context";
|
|
1576
|
+
}
|
|
1577
|
+
budget.reserve("context", contextToUse);
|
|
1578
|
+
}
|
|
1579
|
+
const messages = this.assembleMessages(factsToUse, contextToUse, payloadToUse);
|
|
1580
|
+
const promptText = messages.map((msg) => `${msg.role.toUpperCase()}:
|
|
1581
|
+
${msg.content}`).join("\n\n");
|
|
1582
|
+
return {
|
|
1583
|
+
messages,
|
|
1584
|
+
promptText,
|
|
1585
|
+
allocatedTokens: budget.getAllocated(),
|
|
1586
|
+
isTruncated,
|
|
1587
|
+
truncatedSlot
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
// src/prompts/prompt-registry.ts
|
|
1593
|
+
var PromptRegistry = class {
|
|
1594
|
+
static registry = /* @__PURE__ */ new Map();
|
|
1595
|
+
static register(templateOrOptions) {
|
|
1596
|
+
const template = templateOrOptions instanceof PromptTemplate ? templateOrOptions : new PromptTemplate(templateOrOptions);
|
|
1597
|
+
const existing = this.registry.get(template.id) || [];
|
|
1598
|
+
const filtered = existing.filter((item) => item.version !== template.version);
|
|
1599
|
+
filtered.push(template);
|
|
1600
|
+
filtered.sort((a, b) => b.version - a.version);
|
|
1601
|
+
this.registry.set(template.id, filtered);
|
|
1602
|
+
}
|
|
1603
|
+
static get(id, options) {
|
|
1604
|
+
const templates = this.registry.get(id);
|
|
1605
|
+
if (!templates || templates.length === 0) {
|
|
1606
|
+
throw new Error(`[PromptRegistry] No prompt template registered with id: "${id}"`);
|
|
1607
|
+
}
|
|
1608
|
+
if (options?.version !== void 0) {
|
|
1609
|
+
const match = templates.find((item) => item.version === options.version);
|
|
1610
|
+
if (!match) {
|
|
1611
|
+
throw new Error(
|
|
1612
|
+
`[PromptRegistry] Template "${id}" with version ${options.version} not found`
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
return match;
|
|
1616
|
+
}
|
|
1617
|
+
if (options?.label) {
|
|
1618
|
+
const match = templates.find((item) => item.label === options.label);
|
|
1619
|
+
if (match) {
|
|
1620
|
+
return match;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
return templates[0];
|
|
1624
|
+
}
|
|
1625
|
+
static has(id) {
|
|
1626
|
+
return this.registry.has(id);
|
|
1627
|
+
}
|
|
1628
|
+
static clear() {
|
|
1629
|
+
this.registry.clear();
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
349
1632
|
// Annotate the CommonJS export names for ESM import in node:
|
|
350
1633
|
0 && (module.exports = {
|
|
1634
|
+
AvantGateBudgetExceededError,
|
|
1635
|
+
AvantGateConfigurationError,
|
|
351
1636
|
AvantGateControlLayer,
|
|
1637
|
+
BudgetExceededError,
|
|
1638
|
+
CachedPricingAdapter,
|
|
1639
|
+
ConfigurationError,
|
|
352
1640
|
DEFAULT_MODEL_PRICES,
|
|
1641
|
+
HttpProviderClient,
|
|
1642
|
+
PricingRegistry,
|
|
1643
|
+
PromptBuilder,
|
|
1644
|
+
PromptRegistry,
|
|
1645
|
+
PromptTemplate,
|
|
1646
|
+
SEED_MODEL_PRICES,
|
|
353
1647
|
ZenLLMControlLayer,
|
|
354
1648
|
calculateCostUSD,
|
|
355
1649
|
createAvantGate,
|
|
1650
|
+
createHttpProviderClient,
|
|
356
1651
|
createLLMControlLayer,
|
|
357
1652
|
extractAndCleanJSON,
|
|
1653
|
+
resolveModelPrice,
|
|
1654
|
+
resolveModelPriceAsync,
|
|
358
1655
|
sanitizePII,
|
|
359
1656
|
validateUserInput,
|
|
360
1657
|
validateWithZod
|