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/dist/index.mjs CHANGED
@@ -1,78 +1,220 @@
1
+ import {
2
+ sanitizePII
3
+ } from "./chunk-DVCF4CSV.mjs";
4
+ import {
5
+ extractAndCleanJSON,
6
+ validateWithZod
7
+ } from "./chunk-CO26LNFD.mjs";
8
+
9
+ // src/types.ts
10
+ var ConfigurationError = class extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "ConfigurationError";
14
+ }
15
+ };
16
+ var BudgetExceededError = class extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "BudgetExceededError";
20
+ }
21
+ };
22
+
1
23
  // src/pricing.ts
2
- var DEFAULT_MODEL_PRICES = {
3
- // DeepSeek
4
- "deepseek-chat": {
5
- promptUSDPerMillion: 0.14,
6
- completionUSDPerMillion: 0.28,
7
- cacheHitUSDPerMillion: 0.014
8
- },
9
- "deepseek-reasoner": {
10
- promptUSDPerMillion: 0.55,
11
- completionUSDPerMillion: 2.19,
12
- cacheHitUSDPerMillion: 0.14
13
- },
14
- // Mistral
15
- "mistral-small-latest": {
16
- promptUSDPerMillion: 0.2,
17
- completionUSDPerMillion: 0.6
18
- },
19
- "mistral-large-latest": {
20
- promptUSDPerMillion: 2,
21
- completionUSDPerMillion: 6
24
+ var SEED_MODEL_PRICES = {
25
+ // DeepSeek Direct
26
+ "deepseek-chat": { promptUSDPerMillion: 0.14, completionUSDPerMillion: 0.28, cacheHitUSDPerMillion: 0.014 },
27
+ "deepseek-reasoner": { promptUSDPerMillion: 0.55, completionUSDPerMillion: 2.19, cacheHitUSDPerMillion: 0.14 },
28
+ "deepseek/deepseek-chat": { promptUSDPerMillion: 0.14, completionUSDPerMillion: 0.28, cacheHitUSDPerMillion: 0.014 },
29
+ "deepseek/deepseek-reasoner": { promptUSDPerMillion: 0.55, completionUSDPerMillion: 2.19, cacheHitUSDPerMillion: 0.14 },
30
+ // Mistral AI Direct
31
+ "mistral-small-latest": { promptUSDPerMillion: 0.2, completionUSDPerMillion: 0.6 },
32
+ "mistral-large-latest": { promptUSDPerMillion: 2, completionUSDPerMillion: 6 },
33
+ "mistral/mistral-small-latest": { promptUSDPerMillion: 0.2, completionUSDPerMillion: 0.6 },
34
+ "mistral/mistral-large-latest": { promptUSDPerMillion: 2, completionUSDPerMillion: 6 },
35
+ // OpenAI Direct
36
+ "gpt-4o-mini": { promptUSDPerMillion: 0.15, completionUSDPerMillion: 0.6 },
37
+ "gpt-4o": { promptUSDPerMillion: 2.5, completionUSDPerMillion: 10 },
38
+ "openai/gpt-4o-mini": { promptUSDPerMillion: 0.15, completionUSDPerMillion: 0.6 },
39
+ "openai/gpt-4o": { promptUSDPerMillion: 2.5, completionUSDPerMillion: 10 },
40
+ // OpenRouter Direct
41
+ "openrouter/deepseek/deepseek-chat": { promptUSDPerMillion: 0.14, completionUSDPerMillion: 0.28 },
42
+ "openrouter/deepseek/deepseek-r1": { promptUSDPerMillion: 0.55, completionUSDPerMillion: 2.19 },
43
+ "openrouter/anthropic/claude-3.5-sonnet": { promptUSDPerMillion: 3, completionUSDPerMillion: 15 },
44
+ // Local Ollama (Toujours $0)
45
+ "ollama": { promptUSDPerMillion: 0, completionUSDPerMillion: 0 }
46
+ };
47
+ function normalizeKey(identifier) {
48
+ return identifier.trim().toLowerCase().replace(":", "/");
49
+ }
50
+ var CachedPricingAdapter = class {
51
+ cache = /* @__PURE__ */ new Map();
52
+ ttlMs;
53
+ delegate;
54
+ constructor(delegate, ttlMs = 5 * 60 * 1e3) {
55
+ this.delegate = delegate;
56
+ this.ttlMs = ttlMs;
57
+ }
58
+ async fetchPrice(model, provider) {
59
+ const key = provider ? normalizeKey(`${provider}/${model}`) : normalizeKey(model);
60
+ const cached = this.cache.get(key);
61
+ if (cached && Date.now() < cached.expiresAt) {
62
+ return cached.price;
63
+ }
64
+ try {
65
+ const freshPrice = await this.delegate.fetchPrice(model, provider);
66
+ if (freshPrice) {
67
+ this.cache.set(key, { price: freshPrice, expiresAt: Date.now() + this.ttlMs });
68
+ return freshPrice;
69
+ }
70
+ } catch {
71
+ if (cached) {
72
+ return cached.price;
73
+ }
74
+ }
75
+ return void 0;
76
+ }
77
+ peek(model, provider) {
78
+ const key = provider ? normalizeKey(`${provider}/${model}`) : normalizeKey(model);
79
+ return this.cache.get(key)?.price;
80
+ }
81
+ clearCache() {
82
+ this.cache.clear();
83
+ }
84
+ invalidate(model, provider) {
85
+ if (!model) {
86
+ this.clearCache();
87
+ return;
88
+ }
89
+ const key = provider ? normalizeKey(`${provider}/${model}`) : normalizeKey(model);
90
+ this.cache.delete(key);
91
+ }
92
+ };
93
+ var PricingRegistry = class {
94
+ static prices = /* @__PURE__ */ new Map();
95
+ static adapter;
96
+ static registerPrice(identifier, price) {
97
+ this.prices.set(normalizeKey(identifier), price);
98
+ }
99
+ static registerDistributorPrices(distributor, priceMap) {
100
+ for (const [model, price] of Object.entries(priceMap)) {
101
+ this.prices.set(normalizeKey(`${distributor}/${model}`), price);
102
+ }
103
+ }
104
+ static registerPrices(prices) {
105
+ for (const [key, price] of Object.entries(prices)) {
106
+ this.prices.set(normalizeKey(key), price);
107
+ }
108
+ }
109
+ static getPrice(model, provider) {
110
+ if (provider) {
111
+ const distributorKey = normalizeKey(`${provider}/${model}`);
112
+ const directDistributorPrice = this.prices.get(distributorKey);
113
+ if (directDistributorPrice) {
114
+ return directDistributorPrice;
115
+ }
116
+ }
117
+ return this.prices.get(normalizeKey(model));
118
+ }
119
+ static setAdapter(adapter) {
120
+ this.adapter = adapter;
121
+ }
122
+ static getAdapter() {
123
+ return this.adapter;
124
+ }
125
+ static clear() {
126
+ this.prices.clear();
127
+ this.adapter = void 0;
128
+ }
129
+ /**
130
+ * Réinitialise le registre en chargeant le catalogue d'exemple SEED_MODEL_PRICES.
131
+ */
132
+ static loadSeedPrices() {
133
+ this.registerPrices(SEED_MODEL_PRICES);
134
+ }
135
+ static getAllPrices() {
136
+ const out = {};
137
+ for (const [k, v] of this.prices.entries()) {
138
+ out[k] = v;
139
+ }
140
+ return out;
141
+ }
142
+ };
143
+ var DEFAULT_MODEL_PRICES = new Proxy({}, {
144
+ get(_target, prop) {
145
+ return PricingRegistry.getPrice(prop);
22
146
  },
23
- // OpenAI
24
- "gpt-4o-mini": {
25
- promptUSDPerMillion: 0.15,
26
- completionUSDPerMillion: 0.6
147
+ has(_target, prop) {
148
+ return Boolean(PricingRegistry.getPrice(prop));
27
149
  },
28
- "gpt-4o": {
29
- promptUSDPerMillion: 2.5,
30
- completionUSDPerMillion: 10
150
+ ownKeys() {
151
+ return Object.keys(PricingRegistry.getAllPrices());
31
152
  },
32
- // Ollama (local)
33
- "ollama": {
34
- promptUSDPerMillion: 0,
35
- completionUSDPerMillion: 0
153
+ getOwnPropertyDescriptor(_target, prop) {
154
+ const price = PricingRegistry.getPrice(prop);
155
+ if (price) {
156
+ return { value: price, enumerable: true, configurable: true, writable: false };
157
+ }
158
+ return void 0;
159
+ }
160
+ });
161
+ function resolveModelPrice(model, options) {
162
+ const opts = typeof options === "string" ? { provider: options } : options ?? {};
163
+ if (opts.providerPricing) {
164
+ return opts.providerPricing;
165
+ }
166
+ if (opts.customPricing) {
167
+ if (opts.provider) {
168
+ const customDistributorKey = normalizeKey(`${opts.provider}/${model}`);
169
+ if (opts.customPricing[customDistributorKey]) {
170
+ return opts.customPricing[customDistributorKey];
171
+ }
172
+ }
173
+ if (opts.customPricing[normalizeKey(model)] ?? opts.customPricing[model]) {
174
+ return opts.customPricing[normalizeKey(model)] ?? opts.customPricing[model];
175
+ }
176
+ }
177
+ const activeAdapter = opts.adapter ?? PricingRegistry.getAdapter();
178
+ if (activeAdapter instanceof CachedPricingAdapter) {
179
+ const cached = activeAdapter.peek(model, opts.provider);
180
+ if (cached) {
181
+ return cached;
182
+ }
183
+ }
184
+ const registeredPrice = PricingRegistry.getPrice(model, opts.provider);
185
+ if (registeredPrice) {
186
+ return registeredPrice;
187
+ }
188
+ if (opts.provider === "ollama" || model.toLowerCase().includes("ollama")) {
189
+ return { promptUSDPerMillion: 0, completionUSDPerMillion: 0 };
190
+ }
191
+ return void 0;
192
+ }
193
+ async function resolveModelPriceAsync(model, options) {
194
+ const opts = typeof options === "string" ? { provider: options } : options ?? {};
195
+ const activeAdapter = opts.adapter ?? PricingRegistry.getAdapter();
196
+ if (activeAdapter) {
197
+ try {
198
+ const adapterPrice = await activeAdapter.fetchPrice(model, opts.provider);
199
+ if (adapterPrice) {
200
+ return adapterPrice;
201
+ }
202
+ } catch {
203
+ }
204
+ }
205
+ return resolveModelPrice(model, options);
206
+ }
207
+ function calculateCostUSD(model, promptTokens, completionTokens, cacheHitTokens = 0, options) {
208
+ const pricing = resolveModelPrice(model, options);
209
+ if (!pricing) {
210
+ return 0;
36
211
  }
37
- };
38
- function calculateCostUSD(model, promptTokens, completionTokens, cacheHitTokens = 0) {
39
- const pricing = DEFAULT_MODEL_PRICES[model] ?? {
40
- promptUSDPerMillion: 0.5,
41
- completionUSDPerMillion: 1.5
42
- };
43
212
  const promptCost = promptTokens / 1e6 * pricing.promptUSDPerMillion;
44
213
  const completionCost = completionTokens / 1e6 * pricing.completionUSDPerMillion;
45
214
  const cacheDiscount = cacheHitTokens && pricing.cacheHitUSDPerMillion ? cacheHitTokens / 1e6 * (pricing.promptUSDPerMillion - pricing.cacheHitUSDPerMillion) : 0;
46
215
  return Math.max(0, promptCost + completionCost - cacheDiscount);
47
216
  }
48
217
 
49
- // src/sanitizer.ts
50
- var EMAIL_REGEX = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b/g;
51
- var PHONE_FR_REGEX = /\b(?:(?:\+|00)33|0)\s*[1-9](?:[\s.-]*\d{2}){4}\b/g;
52
- var NIR_SSN_REGEX = /\b[12]\s*\d{2}\s*\d{2}\s*\d{2}\s*\d{3}\s*\d{3}(?:\s*\d{2})?\b/g;
53
- var IBAN_REGEX = /\b[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}([A-Z0-9]?){0,16}\b/g;
54
- function sanitizePII(input) {
55
- let count = 0;
56
- let result = input;
57
- result = result.replace(EMAIL_REGEX, () => {
58
- count++;
59
- return "[REDACTED_EMAIL]";
60
- });
61
- result = result.replace(PHONE_FR_REGEX, () => {
62
- count++;
63
- return "[REDACTED_PHONE]";
64
- });
65
- result = result.replace(NIR_SSN_REGEX, () => {
66
- count++;
67
- return "[REDACTED_NIR]";
68
- });
69
- result = result.replace(IBAN_REGEX, () => {
70
- count++;
71
- return "[REDACTED_IBAN]";
72
- });
73
- return { text: result, maskedCount: count };
74
- }
75
-
76
218
  // src/input-guard.ts
77
219
  var INJECTION_PATTERNS = [
78
220
  /ignore\s+(?:all\s+)?(?:previous|prior)\s+(?:instructions|prompts|rules)/i,
@@ -103,49 +245,144 @@ function validateUserInput(input, options) {
103
245
  return { valid: true };
104
246
  }
105
247
 
106
- // src/response-validator.ts
107
- function extractAndCleanJSON(rawText) {
108
- let cleaned = rawText.trim();
109
- cleaned = cleaned.replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
110
- const codeBlockMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
111
- if (codeBlockMatch && codeBlockMatch[1]) {
112
- cleaned = codeBlockMatch[1].trim();
113
- }
114
- const firstBrace = cleaned.indexOf("{");
115
- const firstBracket = cleaned.indexOf("[");
116
- let startIndex = -1;
117
- if (firstBrace !== -1 && firstBracket !== -1) {
118
- startIndex = Math.min(firstBrace, firstBracket);
119
- } else if (firstBrace !== -1) {
120
- startIndex = firstBrace;
121
- } else if (firstBracket !== -1) {
122
- startIndex = firstBracket;
123
- }
124
- const lastBrace = cleaned.lastIndexOf("}");
125
- const lastBracket = cleaned.lastIndexOf("]");
126
- const endIndex = Math.max(lastBrace, lastBracket);
127
- if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
128
- cleaned = cleaned.slice(startIndex, endIndex + 1);
129
- }
130
- return cleaned;
131
- }
132
- function validateWithZod(rawText, schema) {
133
- const jsonString = extractAndCleanJSON(rawText);
134
- let parsed;
135
- try {
136
- parsed = JSON.parse(jsonString);
137
- } catch (error) {
138
- const sanitized = jsonString.replace(/,\s*([}\]])/g, "$1").replace(/'/g, '"');
139
- parsed = JSON.parse(sanitized);
140
- }
141
- return schema.parse(parsed);
248
+ // src/providers/http-client.ts
249
+ var DEFAULT_BASE_URLS = {
250
+ deepseek: "https://api.deepseek.com/v1",
251
+ mistral: "https://api.mistral.ai/v1",
252
+ openai: "https://api.openai.com/v1",
253
+ ollama: "http://localhost:11434/v1",
254
+ openrouter: "https://openrouter.ai/api/v1"
255
+ };
256
+ var HttpProviderClient = class {
257
+ name;
258
+ baseUrl;
259
+ apiKey;
260
+ providerType;
261
+ constructor(config) {
262
+ this.providerType = config.provider;
263
+ this.name = `${config.provider}-http`;
264
+ this.apiKey = config.apiKey;
265
+ this.baseUrl = config.baseUrl || DEFAULT_BASE_URLS[config.provider] || "https://api.openai.com/v1";
266
+ }
267
+ resolveEndpoint() {
268
+ const trimmed = this.baseUrl.replace(/\/+$/, "");
269
+ if (trimmed.endsWith("/chat/completions")) {
270
+ return trimmed;
271
+ }
272
+ return `${trimmed}/chat/completions`;
273
+ }
274
+ buildHeaders() {
275
+ const headers = {
276
+ "Content-Type": "application/json"
277
+ };
278
+ if (this.apiKey) {
279
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
280
+ }
281
+ if (this.providerType === "openrouter") {
282
+ headers["HTTP-Referer"] = "https://avantgate.dev";
283
+ headers["X-Title"] = "AvantGate";
284
+ }
285
+ return headers;
286
+ }
287
+ buildPayload(options, model) {
288
+ return {
289
+ model,
290
+ messages: options.messages.map((m) => ({
291
+ role: m.role,
292
+ content: m.content
293
+ })),
294
+ temperature: options.temperature ?? 0.2,
295
+ ...options.responseFormat ? { response_format: options.responseFormat } : {}
296
+ };
297
+ }
298
+ extractUsage(usageData) {
299
+ if (!usageData) {
300
+ return void 0;
301
+ }
302
+ return {
303
+ promptTokens: usageData.prompt_tokens,
304
+ completionTokens: usageData.completion_tokens,
305
+ totalTokens: usageData.total_tokens,
306
+ promptCacheHitTokens: usageData.prompt_tokens_details?.cached_tokens
307
+ };
308
+ }
309
+ async complete(options) {
310
+ const endpoint = this.resolveEndpoint();
311
+ const model = options.model ?? "default";
312
+ const headers = this.buildHeaders();
313
+ const body = JSON.stringify(this.buildPayload(options, model));
314
+ const response = await fetch(endpoint, {
315
+ method: "POST",
316
+ headers,
317
+ body
318
+ });
319
+ if (!response.ok) {
320
+ const errorText = await response.text();
321
+ throw new Error(
322
+ `[AvantGate HTTP Provider ${this.providerType}] HTTP ${response.status} ${response.statusText}: ${errorText}`
323
+ );
324
+ }
325
+ const data = await response.json();
326
+ const text = data.choices?.[0]?.message?.content ?? "";
327
+ const usage = this.extractUsage(data.usage);
328
+ return { text, usage };
329
+ }
330
+ };
331
+ function createHttpProviderClient(config) {
332
+ return new HttpProviderClient(config);
142
333
  }
143
334
 
144
335
  // src/control-layer.ts
145
336
  var AvantGateControlLayer = class {
146
337
  config;
338
+ cachedPricingAdapter;
147
339
  constructor(config) {
148
- this.config = config;
340
+ this.config = {
341
+ ...config,
342
+ primary: this.resolveProviderConfig(config.primary),
343
+ fallback: this.resolveProviderConfig(config.fallback),
344
+ emergencyFallback: this.resolveProviderConfig(config.emergencyFallback)
345
+ };
346
+ if (config.pricingAdapter) {
347
+ this.cachedPricingAdapter = new CachedPricingAdapter(
348
+ config.pricingAdapter,
349
+ config.pricingCacheTtlMs ?? 5 * 60 * 1e3
350
+ );
351
+ }
352
+ }
353
+ resolveProviderConfig(provider) {
354
+ if (!provider) return void 0;
355
+ if (provider.client) return provider;
356
+ if (provider.apiKey || provider.baseUrl || provider.provider === "ollama") {
357
+ return {
358
+ ...provider,
359
+ client: createHttpProviderClient(provider)
360
+ };
361
+ }
362
+ return provider;
363
+ }
364
+ findProviderConfig(provider, model) {
365
+ const list = [this.config.primary, this.config.fallback, this.config.emergencyFallback].filter(
366
+ (p) => Boolean(p)
367
+ );
368
+ if (provider) {
369
+ const matchProvider = list.find((p) => p.provider === provider);
370
+ if (matchProvider) return matchProvider;
371
+ }
372
+ if (model) {
373
+ const matchModel = list.find((p) => p.model === model);
374
+ if (matchModel) return matchModel;
375
+ }
376
+ return this.config.primary;
377
+ }
378
+ calculateCost(model, promptTokens, completionTokens, cacheHitTokens = 0, provider) {
379
+ const providerCfg = this.findProviderConfig(provider, model);
380
+ return calculateCostUSD(model, promptTokens, completionTokens, cacheHitTokens, {
381
+ provider: provider ?? providerCfg?.provider,
382
+ providerPricing: providerCfg?.pricing,
383
+ customPricing: this.config.customPricing,
384
+ adapter: this.cachedPricingAdapter
385
+ });
149
386
  }
150
387
  applySecurityGuards(userQuery) {
151
388
  const guard = validateUserInput(userQuery, {
@@ -160,6 +397,46 @@ var AvantGateControlLayer = class {
160
397
  }
161
398
  return userQuery;
162
399
  }
400
+ checkPreflightBudget(estimatedPromptTokens, targetModel, provider) {
401
+ if (this.config.maxTokenBudget !== void 0 && estimatedPromptTokens > this.config.maxTokenBudget) {
402
+ throw new BudgetExceededError(
403
+ `[AvantGate Budget Guard] Pre-flight token budget exceeded: estimated prompt (${estimatedPromptTokens} tokens) exceeds maxTokenBudget (${this.config.maxTokenBudget}).`
404
+ );
405
+ }
406
+ if (this.config.maxCostUSD !== void 0) {
407
+ const isLocalFree = provider === "ollama" || targetModel.toLowerCase().includes("ollama");
408
+ const providerCfg = this.findProviderConfig(provider, targetModel);
409
+ const price = resolveModelPrice(targetModel, {
410
+ provider: provider ?? providerCfg?.provider,
411
+ providerPricing: providerCfg?.pricing,
412
+ customPricing: this.config.customPricing,
413
+ adapter: this.cachedPricingAdapter
414
+ });
415
+ if (!isLocalFree && !price) {
416
+ throw new ConfigurationError(
417
+ `[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.`
418
+ );
419
+ }
420
+ const estimatedPromptCost = this.calculateCost(targetModel, estimatedPromptTokens, 0, 0, provider);
421
+ if (estimatedPromptCost > this.config.maxCostUSD) {
422
+ throw new BudgetExceededError(
423
+ `[AvantGate Budget Guard] Pre-flight cost budget exceeded: estimated prompt cost ($${estimatedPromptCost.toFixed(6)}) exceeds maxCostUSD ($${this.config.maxCostUSD}).`
424
+ );
425
+ }
426
+ }
427
+ }
428
+ checkPostExecutionBudget(tokensTotal, costUSD) {
429
+ if (this.config.maxTokenBudget !== void 0 && tokensTotal > this.config.maxTokenBudget) {
430
+ throw new BudgetExceededError(
431
+ `[AvantGate Budget Guard] Execution total tokens (${tokensTotal}) exceeded maxTokenBudget (${this.config.maxTokenBudget}).`
432
+ );
433
+ }
434
+ if (this.config.maxCostUSD !== void 0 && costUSD > this.config.maxCostUSD) {
435
+ throw new BudgetExceededError(
436
+ `[AvantGate Budget Guard] Execution cost ($${costUSD.toFixed(6)}) exceeded maxCostUSD ($${this.config.maxCostUSD}).`
437
+ );
438
+ }
439
+ }
163
440
  buildMessages(systemPrompt, query) {
164
441
  const messages = [];
165
442
  if (systemPrompt) {
@@ -172,7 +449,12 @@ var AvantGateControlLayer = class {
172
449
  const promptTokens = rawUsage?.promptTokens ?? Math.ceil(query.length / 4);
173
450
  const completionTokens = rawUsage?.completionTokens ?? Math.ceil(text.length / 4);
174
451
  const totalTokens = rawUsage?.totalTokens ?? promptTokens + completionTokens;
175
- return { promptTokens, completionTokens, totalTokens };
452
+ return {
453
+ promptTokens,
454
+ completionTokens,
455
+ totalTokens,
456
+ promptCacheHitTokens: rawUsage?.promptCacheHitTokens
457
+ };
176
458
  }
177
459
  getProviderChain() {
178
460
  return [
@@ -184,7 +466,7 @@ var AvantGateControlLayer = class {
184
466
  executeSimulation(query, systemPrompt) {
185
467
  const promptTokens = Math.ceil(((systemPrompt?.length ?? 0) + query.length) / 4);
186
468
  const completionTokens = 50;
187
- const cost = calculateCostUSD(this.config.primary.model, promptTokens, completionTokens);
469
+ const cost = this.calculateCost(this.config.primary.model, promptTokens, completionTokens);
188
470
  return {
189
471
  text: `[AvantGate In-Process Engine] Response simulation for model: ${this.config.primary.model}`,
190
472
  tokens: {
@@ -213,14 +495,15 @@ var AvantGateControlLayer = class {
213
495
  attempts: 1
214
496
  };
215
497
  }
216
- async executeProviderPipeline(messages, query, temperature) {
498
+ async executeProviderPipeline(messages, query, temperature, modelOverride) {
217
499
  const chain = this.getProviderChain();
218
500
  let lastError;
219
501
  for (let index = 0; index < chain.length; index++) {
220
502
  const providerConfig = chain[index];
503
+ const targetModel = index === 0 && modelOverride ? modelOverride : providerConfig.model;
221
504
  try {
222
505
  const response = await providerConfig.client.complete({
223
- model: providerConfig.model,
506
+ model: targetModel,
224
507
  messages,
225
508
  temperature: temperature ?? 0.2
226
509
  });
@@ -228,7 +511,7 @@ var AvantGateControlLayer = class {
228
511
  return {
229
512
  responseText: response.text,
230
513
  usage,
231
- modelUsed: providerConfig.model,
514
+ modelUsed: targetModel,
232
515
  failoverOccurred: index > 0,
233
516
  attempts: index + 1
234
517
  };
@@ -239,10 +522,11 @@ var AvantGateControlLayer = class {
239
522
  throw lastError;
240
523
  }
241
524
  assembleResult(output) {
242
- const costUSD = calculateCostUSD(
525
+ const costUSD = this.calculateCost(
243
526
  output.modelUsed,
244
527
  output.usage.promptTokens,
245
- output.usage.completionTokens
528
+ output.usage.completionTokens,
529
+ output.usage.promptCacheHitTokens ?? 0
246
530
  );
247
531
  return {
248
532
  text: output.responseText,
@@ -271,18 +555,27 @@ var AvantGateControlLayer = class {
271
555
  });
272
556
  }
273
557
  /**
274
- * Exécute une requête avec garde d'entrée, masquage PII, et calcul des coûts.
558
+ * Exécute une requête avec garde d'entrée, masquage PII, garde pré-vol et calcul des coûts.
275
559
  */
276
560
  async execute(options) {
277
561
  const sanitizedQuery = this.applySecurityGuards(options.userQuery);
278
562
  const messages = this.buildMessages(options.systemPrompt, sanitizedQuery);
563
+ const promptLength = (options.systemPrompt?.length ?? 0) + sanitizedQuery.length;
564
+ const estimatedPromptTokens = Math.ceil(promptLength / 4);
565
+ this.checkPreflightBudget(estimatedPromptTokens, this.config.primary.model, this.config.primary.provider);
279
566
  if (!options.providerOverride && this.getProviderChain().length === 0) {
280
- const simResult = this.executeSimulation(sanitizedQuery, options.systemPrompt);
281
- await this.notifyAuditSink(simResult);
282
- return simResult;
567
+ if (this.config.mockSimulation) {
568
+ const simResult = this.executeSimulation(sanitizedQuery, options.systemPrompt);
569
+ await this.notifyAuditSink(simResult);
570
+ return simResult;
571
+ }
572
+ throw new ConfigurationError(
573
+ "[AvantGate Configuration Error] No active LLM provider configured. Provide a client implementing LLMProviderPort or configure credentials (apiKey / baseUrl)."
574
+ );
283
575
  }
284
576
  const output = options.providerOverride ? await this.executeOverrideProvider(options.providerOverride, messages, sanitizedQuery, options.temperature) : await this.executeProviderPipeline(messages, sanitizedQuery, options.temperature);
285
577
  const result = this.assembleResult(output);
578
+ this.checkPostExecutionBudget(result.tokens.total, result.costUSD);
286
579
  await this.notifyAuditSink(result);
287
580
  return result;
288
581
  }
@@ -296,7 +589,13 @@ var AvantGateControlLayer = class {
296
589
  temperature: options.temperature ?? 0.1,
297
590
  providerOverride: options.providerOverride
298
591
  });
299
- const parsedData = validateWithZod(rawResult.text, options.schema);
592
+ const isFinancial = Boolean(
593
+ this.config.features?.finance?.enableFrenchAccounting || this.config.features?.finance
594
+ );
595
+ const parsedData = validateWithZod(rawResult.text, options.schema, {
596
+ financialNormalizer: isFinancial,
597
+ jurisdiction: this.config.features?.finance?.jurisdiction
598
+ });
300
599
  return {
301
600
  data: parsedData,
302
601
  rawText: rawResult.text,
@@ -306,19 +605,452 @@ var AvantGateControlLayer = class {
306
605
  failoverOccurred: rawResult.failoverOccurred
307
606
  };
308
607
  }
608
+ /**
609
+ * Méthode unifiée de premier niveau pour l'extraction structurée sans code boilerplate.
610
+ * Gère le failover multi-fournisseurs, les retries, la validation Zod et la normalisation financière.
611
+ */
612
+ async generateStructuredOutput(options) {
613
+ const maxRetries = options.maxRetries ?? this.config.retryOptions?.maxRetries ?? 2;
614
+ const modelToUse = options.model ?? this.config.primary.model;
615
+ const processedMessages = options.messages.map((msg) => {
616
+ if (msg.role === "user") {
617
+ return { ...msg, content: this.applySecurityGuards(msg.content) };
618
+ }
619
+ return msg;
620
+ });
621
+ const promptLength = processedMessages.reduce((sum, msg) => sum + msg.content.length, 0);
622
+ const estimatedPromptTokens = Math.ceil(promptLength / 4);
623
+ this.checkPreflightBudget(estimatedPromptTokens, modelToUse);
624
+ let lastError;
625
+ let accumulatedPromptTokens = 0;
626
+ let accumulatedCompletionTokens = 0;
627
+ let failoverOccurred = false;
628
+ let modelUsed = modelToUse;
629
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
630
+ try {
631
+ let responseText = "";
632
+ let attemptPromptTokens = 0;
633
+ let attemptCompletionTokens = 0;
634
+ if (options.providerOverride) {
635
+ const res = await options.providerOverride.complete({
636
+ model: modelToUse,
637
+ messages: processedMessages,
638
+ temperature: options.temperature ?? 0.1
639
+ });
640
+ responseText = res.text;
641
+ const usage = this.resolveTokens(res.usage, JSON.stringify(processedMessages), responseText);
642
+ attemptPromptTokens = usage.promptTokens;
643
+ attemptCompletionTokens = usage.completionTokens;
644
+ modelUsed = modelToUse;
645
+ } else if (this.getProviderChain().length === 0) {
646
+ if (this.config.mockSimulation) {
647
+ const sim = this.executeSimulation(JSON.stringify(processedMessages));
648
+ responseText = sim.text;
649
+ attemptPromptTokens = sim.tokens.prompt;
650
+ attemptCompletionTokens = sim.tokens.completion;
651
+ } else {
652
+ throw new ConfigurationError(
653
+ "[AvantGate Configuration Error] No active LLM provider configured. Provide a client implementing LLMProviderPort or configure credentials (apiKey / baseUrl)."
654
+ );
655
+ }
656
+ } else {
657
+ const output = await this.executeProviderPipeline(
658
+ processedMessages,
659
+ JSON.stringify(processedMessages),
660
+ options.temperature ?? 0.1,
661
+ options.model
662
+ );
663
+ responseText = output.responseText;
664
+ attemptPromptTokens = output.usage.promptTokens;
665
+ attemptCompletionTokens = output.usage.completionTokens;
666
+ modelUsed = output.modelUsed;
667
+ failoverOccurred = output.failoverOccurred;
668
+ }
669
+ accumulatedPromptTokens += attemptPromptTokens;
670
+ accumulatedCompletionTokens += attemptCompletionTokens;
671
+ const isFinancial = options.financialNormalizer ?? Boolean(
672
+ this.config.features?.finance?.enableFrenchAccounting || this.config.features?.finance
673
+ );
674
+ const parsedData = validateWithZod(responseText, options.schema, {
675
+ financialNormalizer: isFinancial,
676
+ jurisdiction: this.config.features?.finance?.jurisdiction
677
+ });
678
+ const totalTokens = accumulatedPromptTokens + accumulatedCompletionTokens;
679
+ const costUSD = this.calculateCost(modelUsed, accumulatedPromptTokens, accumulatedCompletionTokens);
680
+ this.checkPostExecutionBudget(totalTokens, costUSD);
681
+ const result = {
682
+ data: parsedData,
683
+ rawText: responseText,
684
+ tokens: {
685
+ prompt: accumulatedPromptTokens,
686
+ completion: accumulatedCompletionTokens,
687
+ total: totalTokens
688
+ },
689
+ costUSD,
690
+ modelUsed,
691
+ failoverOccurred
692
+ };
693
+ await this.notifyAuditSink({
694
+ text: responseText,
695
+ tokens: result.tokens,
696
+ costUSD: result.costUSD,
697
+ modelUsed: result.modelUsed,
698
+ failoverOccurred: result.failoverOccurred,
699
+ attempts: attempt + 1
700
+ });
701
+ return result;
702
+ } catch (err) {
703
+ lastError = err;
704
+ if (attempt < maxRetries) {
705
+ const delay = (this.config.retryOptions?.initialDelayMs ?? 200) * Math.pow(this.config.retryOptions?.backoffFactor ?? 1.5, attempt);
706
+ await new Promise((resolve) => setTimeout(resolve, delay));
707
+ }
708
+ }
709
+ }
710
+ throw lastError;
711
+ }
309
712
  };
310
713
  function createLLMControlLayer(config) {
311
714
  return new AvantGateControlLayer(config);
312
715
  }
313
716
  var createAvantGate = createLLMControlLayer;
717
+
718
+ // src/prompts/prompt-template.ts
719
+ var DEFAULT_JAILBREAK_PATTERNS = [
720
+ /ignore\s+(all\s+)?(previous|prior)\s+(instructions|rules)/i,
721
+ /disregard\s+(all\s+)?(previous|prior)\s+(instructions|rules)/i,
722
+ /system\s+override/i,
723
+ /you\s+are\s+now\s+(unrestricted|DAN|jailbroken)/i,
724
+ /<\s*\|\s*im_start\s*\|>/i,
725
+ /\[SYSTEM_PROMPT\]/i
726
+ ];
727
+ var PromptTemplate = class {
728
+ id;
729
+ version;
730
+ label;
731
+ description;
732
+ template;
733
+ inputSchema;
734
+ blockPatterns;
735
+ antiInjectionEnabled;
736
+ customSanitizer;
737
+ constructor(options) {
738
+ this.id = options.id;
739
+ this.version = options.version;
740
+ this.label = options.label || "production";
741
+ this.description = options.description;
742
+ this.template = options.template;
743
+ this.inputSchema = options.inputSchema;
744
+ this.antiInjectionEnabled = options.antiInjection?.enabled !== false;
745
+ this.blockPatterns = options.antiInjection?.blockPatterns || DEFAULT_JAILBREAK_PATTERNS;
746
+ this.customSanitizer = options.antiInjection?.sanitizer;
747
+ }
748
+ validateUserInput(variables) {
749
+ const threats = [];
750
+ if (this.inputSchema) {
751
+ const parsed = this.inputSchema.safeParse(variables);
752
+ if (!parsed.success) {
753
+ return {
754
+ isValid: false,
755
+ threats: parsed.error.issues.map(
756
+ (issue) => `${issue.path.join(".")}: ${issue.message}`
757
+ )
758
+ };
759
+ }
760
+ }
761
+ if (this.antiInjectionEnabled) {
762
+ for (const [key, val] of Object.entries(variables)) {
763
+ if (typeof val === "string") {
764
+ for (const pattern of this.blockPatterns) {
765
+ if (pattern.test(val)) {
766
+ threats.push(
767
+ `Potential prompt injection in variable "${key}" matching pattern ${pattern}`
768
+ );
769
+ }
770
+ }
771
+ }
772
+ }
773
+ }
774
+ return { isValid: threats.length === 0, threats };
775
+ }
776
+ format(variables) {
777
+ const check = this.validateUserInput(variables);
778
+ if (!check.isValid) {
779
+ throw new Error(
780
+ `[PromptTemplate:${this.id}] Validation failed: ${check.threats.join(", ")}`
781
+ );
782
+ }
783
+ const interpolated = this.template.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (_, key) => {
784
+ let value = variables[key];
785
+ if (value === void 0 || value === null) {
786
+ return "";
787
+ }
788
+ let stringValue = typeof value === "object" ? JSON.stringify(value) : String(value);
789
+ if (this.customSanitizer) {
790
+ stringValue = this.customSanitizer(stringValue);
791
+ }
792
+ return stringValue;
793
+ });
794
+ return interpolated.split("\n").filter((line, index, arr) => line.trim() !== "" || index > 0 && arr[index - 1].trim() !== "").join("\n");
795
+ }
796
+ };
797
+
798
+ // src/prompts/prompt-builder.ts
799
+ import { z } from "zod";
800
+ var PromptBuilder = class {
801
+ personaSlot = "";
802
+ rulesSlot = [];
803
+ retryHintSlot = "";
804
+ fewShotSlot = [];
805
+ pinnedFactsSlot = "";
806
+ contextSlot = "";
807
+ userPayloadSlot = "";
808
+ schemaContractText = "";
809
+ constructor(templateOrId) {
810
+ if (templateOrId instanceof PromptTemplate) {
811
+ this.personaSlot = templateOrId.template;
812
+ } else if (typeof templateOrId === "string") {
813
+ this.personaSlot = templateOrId.trim();
814
+ }
815
+ }
816
+ withPersona(persona) {
817
+ this.personaSlot = persona.trim();
818
+ return this;
819
+ }
820
+ withRules(rules) {
821
+ if (Array.isArray(rules)) {
822
+ this.rulesSlot.push(...rules.map((rule) => rule.trim()).filter(Boolean));
823
+ } else if (rules) {
824
+ this.rulesSlot.push(rules.trim());
825
+ }
826
+ return this;
827
+ }
828
+ withRetryHint(hint) {
829
+ this.retryHintSlot = hint.trim();
830
+ return this;
831
+ }
832
+ withFewShot(examples) {
833
+ this.fewShotSlot = examples;
834
+ return this;
835
+ }
836
+ withPinnedFacts(facts) {
837
+ if (!facts) return this;
838
+ if (typeof facts === "string") {
839
+ this.pinnedFactsSlot = facts.trim();
840
+ return this;
841
+ }
842
+ 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");
843
+ return this;
844
+ }
845
+ withContext(context) {
846
+ this.contextSlot = context.trim();
847
+ return this;
848
+ }
849
+ withUserPayload(payload) {
850
+ this.userPayloadSlot = payload.trim();
851
+ return this;
852
+ }
853
+ schemaContract(schema, options) {
854
+ let shapeDescription = "JSON Object";
855
+ if (schema instanceof z.ZodObject) {
856
+ shapeDescription = JSON.stringify(Object.keys(schema.shape));
857
+ }
858
+ this.schemaContractText = [
859
+ `[DIRECTIVE DE CONTRAT DE SORTIE JSON STRICT]`,
860
+ `Tu DOIS renvoyer UNIQUEMENT un objet JSON valide, sans balises markdown (\`\`\`json), sans texte avant ou apr\xE8s.`,
861
+ `Champs obligatoires et types attendus :`,
862
+ options?.schemaName ? `Sch\xE9ma : ${options.schemaName}` : "",
863
+ shapeDescription
864
+ ].filter(Boolean).join("\n");
865
+ return this;
866
+ }
867
+ assembleMessages(pinnedFacts, context, userPayload) {
868
+ const messages = [];
869
+ let system0 = this.personaSlot || "Tu es un assistant expert.";
870
+ if (this.schemaContractText) {
871
+ system0 += `
872
+
873
+ ${this.schemaContractText}`;
874
+ }
875
+ messages.push({ role: "system", content: system0 });
876
+ const dynamicParts = [];
877
+ if (this.rulesSlot.length > 0) {
878
+ dynamicParts.push(
879
+ `[R\xC8GLES ET CONSIGNES M\xC9TIER]
880
+ ${this.rulesSlot.map((rule, idx) => `${idx + 1}. ${rule}`).join("\n")}`
881
+ );
882
+ }
883
+ if (this.retryHintSlot) {
884
+ dynamicParts.push(`[INSTRUCTION DE CORRECTION / RETRY]
885
+ ${this.retryHintSlot}`);
886
+ }
887
+ if (dynamicParts.length > 0) {
888
+ messages.push({ role: "system", content: dynamicParts.join("\n\n") });
889
+ }
890
+ if (this.fewShotSlot.length > 0) {
891
+ const examplesContent = this.fewShotSlot.map(
892
+ (ex, idx) => `[Exemple ${idx + 1}]
893
+ Question: ${ex.question}
894
+ R\xE9ponse attendue: ${ex.answer}`
895
+ ).join("\n\n");
896
+ messages.push({ role: "system", content: `[EXEMPLES DE R\xC9F\xC9RENCE]
897
+ ${examplesContent}` });
898
+ }
899
+ const userParts = [];
900
+ if (pinnedFacts) {
901
+ userParts.push(`[FAITS STRUCTUR\xC9S]
902
+ ${pinnedFacts}`);
903
+ }
904
+ if (context) {
905
+ userParts.push(`[CONTEXTE DOCUMENTAIRE]
906
+ ${context}`);
907
+ }
908
+ if (userPayload) {
909
+ const labelNeeded = Boolean(pinnedFacts || context);
910
+ userParts.push(labelNeeded ? `[DONN\xC9ES \xC0 ANALYSER]
911
+ ${userPayload}` : userPayload);
912
+ }
913
+ messages.push({ role: "user", content: userParts.join("\n\n") });
914
+ return messages;
915
+ }
916
+ toMessages() {
917
+ return this.assembleMessages(this.pinnedFactsSlot, this.contextSlot, this.userPayloadSlot);
918
+ }
919
+ build(budget) {
920
+ if (!budget) {
921
+ const messages2 = this.toMessages();
922
+ const promptText2 = messages2.map((msg) => `${msg.role.toUpperCase()}:
923
+ ${msg.content}`).join("\n\n");
924
+ return {
925
+ messages: messages2,
926
+ promptText: promptText2,
927
+ allocatedTokens: Math.ceil(promptText2.length / 4),
928
+ isTruncated: false
929
+ };
930
+ }
931
+ budget.reset();
932
+ let isTruncated = false;
933
+ let truncatedSlot;
934
+ let system0 = this.personaSlot || "Tu es un assistant expert.";
935
+ if (this.schemaContractText) {
936
+ system0 += `
937
+
938
+ ${this.schemaContractText}`;
939
+ }
940
+ budget.forceReserve("persona", system0);
941
+ if (this.rulesSlot.length > 0 || this.retryHintSlot) {
942
+ const rulesText = this.rulesSlot.map((rule, idx) => `${idx + 1}. ${rule}`).join("\n");
943
+ budget.forceReserve("rules", `${rulesText}
944
+ ${this.retryHintSlot}`);
945
+ }
946
+ let payloadToUse = this.userPayloadSlot;
947
+ let factsToUse = this.pinnedFactsSlot;
948
+ let contextToUse = this.contextSlot;
949
+ if (payloadToUse) {
950
+ const payloadCost = budget.count(payloadToUse);
951
+ if (payloadCost > budget.remaining()) {
952
+ const charLimit = Math.max(100, budget.remaining() * 4);
953
+ payloadToUse = payloadToUse.slice(0, charLimit);
954
+ isTruncated = true;
955
+ truncatedSlot = "user";
956
+ }
957
+ budget.reserve("user", payloadToUse);
958
+ }
959
+ if (factsToUse) {
960
+ const factsCost = budget.count(factsToUse);
961
+ if (factsCost > budget.remaining()) {
962
+ const charLimit = Math.max(50, budget.remaining() * 4);
963
+ factsToUse = factsToUse.slice(0, charLimit);
964
+ isTruncated = true;
965
+ truncatedSlot = "pinnedFacts";
966
+ }
967
+ budget.reserve("pinnedFacts", factsToUse);
968
+ }
969
+ if (contextToUse) {
970
+ const contextCost = budget.count(contextToUse);
971
+ if (contextCost > budget.remaining()) {
972
+ const charLimit = Math.max(0, budget.remaining() * 4);
973
+ contextToUse = contextToUse.slice(0, charLimit);
974
+ isTruncated = true;
975
+ truncatedSlot = "context";
976
+ }
977
+ budget.reserve("context", contextToUse);
978
+ }
979
+ const messages = this.assembleMessages(factsToUse, contextToUse, payloadToUse);
980
+ const promptText = messages.map((msg) => `${msg.role.toUpperCase()}:
981
+ ${msg.content}`).join("\n\n");
982
+ return {
983
+ messages,
984
+ promptText,
985
+ allocatedTokens: budget.getAllocated(),
986
+ isTruncated,
987
+ truncatedSlot
988
+ };
989
+ }
990
+ };
991
+
992
+ // src/prompts/prompt-registry.ts
993
+ var PromptRegistry = class {
994
+ static registry = /* @__PURE__ */ new Map();
995
+ static register(templateOrOptions) {
996
+ const template = templateOrOptions instanceof PromptTemplate ? templateOrOptions : new PromptTemplate(templateOrOptions);
997
+ const existing = this.registry.get(template.id) || [];
998
+ const filtered = existing.filter((item) => item.version !== template.version);
999
+ filtered.push(template);
1000
+ filtered.sort((a, b) => b.version - a.version);
1001
+ this.registry.set(template.id, filtered);
1002
+ }
1003
+ static get(id, options) {
1004
+ const templates = this.registry.get(id);
1005
+ if (!templates || templates.length === 0) {
1006
+ throw new Error(`[PromptRegistry] No prompt template registered with id: "${id}"`);
1007
+ }
1008
+ if (options?.version !== void 0) {
1009
+ const match = templates.find((item) => item.version === options.version);
1010
+ if (!match) {
1011
+ throw new Error(
1012
+ `[PromptRegistry] Template "${id}" with version ${options.version} not found`
1013
+ );
1014
+ }
1015
+ return match;
1016
+ }
1017
+ if (options?.label) {
1018
+ const match = templates.find((item) => item.label === options.label);
1019
+ if (match) {
1020
+ return match;
1021
+ }
1022
+ }
1023
+ return templates[0];
1024
+ }
1025
+ static has(id) {
1026
+ return this.registry.has(id);
1027
+ }
1028
+ static clear() {
1029
+ this.registry.clear();
1030
+ }
1031
+ };
314
1032
  export {
1033
+ BudgetExceededError as AvantGateBudgetExceededError,
1034
+ ConfigurationError as AvantGateConfigurationError,
315
1035
  AvantGateControlLayer,
1036
+ BudgetExceededError,
1037
+ CachedPricingAdapter,
1038
+ ConfigurationError,
316
1039
  DEFAULT_MODEL_PRICES,
1040
+ HttpProviderClient,
1041
+ PricingRegistry,
1042
+ PromptBuilder,
1043
+ PromptRegistry,
1044
+ PromptTemplate,
1045
+ SEED_MODEL_PRICES,
317
1046
  AvantGateControlLayer as ZenLLMControlLayer,
318
1047
  calculateCostUSD,
319
1048
  createAvantGate,
1049
+ createHttpProviderClient,
320
1050
  createLLMControlLayer,
321
1051
  extractAndCleanJSON,
1052
+ resolveModelPrice,
1053
+ resolveModelPriceAsync,
322
1054
  sanitizePII,
323
1055
  validateUserInput,
324
1056
  validateWithZod