vern-llm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,742 @@
1
+ "use strict";
2
+ //#region rolldown:runtime
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+
24
+ //#endregion
25
+ const crypto = __toESM(require("crypto"));
26
+
27
+ //#region src/types.ts
28
+ var LLMError = class extends Error {
29
+ constructor(message, type, status, issues) {
30
+ super(message);
31
+ this.type = type;
32
+ this.status = status;
33
+ this.issues = issues;
34
+ this.name = "LLMError";
35
+ }
36
+ };
37
+ function isLLMError(err) {
38
+ return err instanceof LLMError;
39
+ }
40
+ /**
41
+ * Trivial default so the package works out of the box with no external deps
42
+ * Not shared across processes, swap in Redis/Upstash/etc for production
43
+ */
44
+ var InMemoryCacheAdapter = class {
45
+ store = new Map();
46
+ async get(key) {
47
+ const entry = this.store.get(key);
48
+ if (!entry) return null;
49
+ if (Date.now() > entry.expiresAt) {
50
+ this.store.delete(key);
51
+ return null;
52
+ }
53
+ return entry.value;
54
+ }
55
+ async set(key, value, ttl) {
56
+ this.store.set(key, {
57
+ value,
58
+ expiresAt: Date.now() + ttl * 1e3
59
+ });
60
+ }
61
+ };
62
+
63
+ //#endregion
64
+ //#region src/circuitBreaker.ts
65
+ /**
66
+ * Per retry VernLLM-instance circuit breaker. Tracks consecutive failures across
67
+ * calls. Once the threshold is hit, short-circuits new calls with an
68
+ * LLMError('circuit_open') instead of hitting the provider, until the
69
+ * cooldown elapses and a single trial call is allowed through
70
+ */
71
+ var CircuitBreaker = class {
72
+ state = "closed";
73
+ consecutiveFailures = 0;
74
+ openedAt = 0;
75
+ threshold;
76
+ cooldownMs;
77
+ constructor(options = {}) {
78
+ this.threshold = options.threshold ?? 5;
79
+ this.cooldownMs = options.cooldownMs ?? 3e4;
80
+ }
81
+ /** Throws if the circuit is open and the cooldown hasnt elapsed */
82
+ assertClosed() {
83
+ if (this.state !== "open") return;
84
+ const elapsed = Date.now() - this.openedAt;
85
+ if (elapsed >= this.cooldownMs) {
86
+ this.state = "half-open";
87
+ return;
88
+ }
89
+ throw new LLMError(`Circuit open — provider has failed ${this.consecutiveFailures} times in a row. Retry in ${Math.ceil((this.cooldownMs - elapsed) / 1e3)}s.`, "circuit_open");
90
+ }
91
+ recordSuccess() {
92
+ this.consecutiveFailures = 0;
93
+ this.state = "closed";
94
+ }
95
+ recordFailure() {
96
+ this.consecutiveFailures += 1;
97
+ if (this.state === "half-open") {
98
+ this.state = "open";
99
+ this.openedAt = Date.now();
100
+ return;
101
+ }
102
+ if (this.consecutiveFailures >= this.threshold) {
103
+ this.state = "open";
104
+ this.openedAt = Date.now();
105
+ }
106
+ }
107
+ getState() {
108
+ return this.state;
109
+ }
110
+ };
111
+
112
+ //#endregion
113
+ //#region src/logger.ts
114
+ /**
115
+ * Default logger. `debug` is gated by the `debug` option on VernLLM
116
+ * warn/error always fire since they indicate real problems (retries, cache failures)
117
+ */
118
+ var ConsoleLogger = class {
119
+ constructor(debugEnabled) {
120
+ this.debugEnabled = debugEnabled;
121
+ }
122
+ debug(message) {
123
+ if (this.debugEnabled) console.debug(message);
124
+ }
125
+ warn(message) {
126
+ console.warn(message);
127
+ }
128
+ error(message, meta) {
129
+ console.error(message, meta ?? "");
130
+ }
131
+ };
132
+
133
+ //#endregion
134
+ //#region src/internal/vernLLM.utilts.ts
135
+ function defaultParseJson(content) {
136
+ try {
137
+ return JSON.parse(content);
138
+ } catch {
139
+ return void 0;
140
+ }
141
+ }
142
+ /**
143
+ * Looks inside an unknown error value and pulls out an http status code
144
+ * if one is present. Checks the status field first then the status code
145
+ * field since different client libraries use different names for this.
146
+ * Returns undefined when the error is not an object or carries no status
147
+ */
148
+ function extractStatus(err) {
149
+ if (!err || typeof err !== "object") return void 0;
150
+ const error = err;
151
+ if (typeof error.status === "number") return error.status;
152
+ if (typeof error.statusCode === "number") return error.statusCode;
153
+ return void 0;
154
+ }
155
+ /**
156
+ * Runs an async function and cancels it if it takes longer than the given
157
+ * timeout. Creates an internal abort controller that fires after the
158
+ * timeout elapses, and combines it with any external signal the caller
159
+ * passed in so either one can cancel the underlying call. The internal
160
+ * timer is always cleared afterward, whether the function succeeds,
161
+ * fails, or is aborted, so nothing is left running in the background
162
+ */
163
+ async function withTimeout(fn, timeoutMs, externalSignal) {
164
+ const controller = new AbortController();
165
+ const timer = setTimeout(() => {
166
+ controller.abort();
167
+ }, timeoutMs);
168
+ const signal = externalSignal ? AbortSignal.any([externalSignal, controller.signal]) : controller.signal;
169
+ try {
170
+ return await fn(signal);
171
+ } finally {
172
+ clearTimeout(timer);
173
+ }
174
+ }
175
+ /**
176
+ * Exponential backoff with jitter, capped at maxDelayMs.
177
+ * Jitter avoids thundering-herd retries when many callers back off in lockstep,
178
+ * the cap prevents unbounded delays when maxRetries is high
179
+ */
180
+ function getBackoffDelay(baseDelayMs, attempt, maxDelayMs = 1e4) {
181
+ const exp = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
182
+ return exp / 2 + Math.random() * (exp / 2);
183
+ }
184
+ /**
185
+ * Pauses execution for the given delay before a retry attempt. If an
186
+ * abort signal is provided and it fires while waiting, the pending
187
+ * timer is cancelled immediately and the wait rejects right away with
188
+ * an aborted error instead of continuing to sit idle until the delay
189
+ * would have finished on its own
190
+ */
191
+ async function waitForRetry(delay, signal) {
192
+ await new Promise((resolve, reject) => {
193
+ const onAbort = () => {
194
+ clearTimeout(timer);
195
+ reject(new LLMError("Operation aborted", "aborted"));
196
+ };
197
+ const timer = setTimeout(() => {
198
+ signal?.removeEventListener("abort", onAbort);
199
+ resolve();
200
+ }, delay);
201
+ signal?.addEventListener("abort", onAbort, { once: true });
202
+ });
203
+ }
204
+
205
+ //#endregion
206
+ //#region src/vernLLM.ts
207
+ /**
208
+ * A resilient wrapper around an LLM chat completions client, this is VernLLM!
209
+ *
210
+ * Adds retry with exponential backoff and jitter, per-attempt timeouts,
211
+ * an optional circuit breaker, JSON parsing with optional schema
212
+ * validation, usage tracking, and an optional response cache, all
213
+ * configurable, all opt-in beyond sensible defaults
214
+ */
215
+ var VernLLM = class {
216
+ client;
217
+ model;
218
+ maxRetries;
219
+ timeoutMs;
220
+ baseDelayMs;
221
+ defaultMaxTokens;
222
+ cache;
223
+ nonRetryableStatus;
224
+ parseJson;
225
+ onUsage;
226
+ logger;
227
+ breaker;
228
+ /**
229
+ * @param options: Client, model, and all tunables (retries, timeout,
230
+ * backoff, cache, circuit breaker, logger, etc). See VernLLMOptions in `types.ts`
231
+ * for individual defaults
232
+ */
233
+ constructor(options) {
234
+ this.client = options.client;
235
+ this.model = options.model;
236
+ const retryConfig = this.resolveRetryConfig(options);
237
+ this.maxRetries = retryConfig.maxRetries;
238
+ this.timeoutMs = retryConfig.timeoutMs;
239
+ this.baseDelayMs = retryConfig.baseDelayMs;
240
+ this.defaultMaxTokens = retryConfig.defaultMaxTokens;
241
+ this.cache = options.cache ?? new InMemoryCacheAdapter();
242
+ this.nonRetryableStatus = options.nonRetryableStatus ?? [
243
+ 400,
244
+ 401,
245
+ 403
246
+ ];
247
+ this.parseJson = options.parseJson ?? defaultParseJson;
248
+ this.onUsage = options.onUsage;
249
+ this.logger = this.resolveLogger(options);
250
+ this.breaker = this.resolveCircuitBreaker(options);
251
+ }
252
+ /**
253
+ * Resolves retry/timeout/token defaults from the given options,
254
+ * falling back to the librarys built-in defaults for anything unset
255
+ */
256
+ resolveRetryConfig(options) {
257
+ return {
258
+ maxRetries: options.maxRetries ?? 1,
259
+ timeoutMs: options.timeoutMs ?? 25e3,
260
+ baseDelayMs: options.baseDelayMs ?? 500,
261
+ defaultMaxTokens: options.defaultMaxTokens ?? 1e3
262
+ };
263
+ }
264
+ /**
265
+ * Returns the caller supplied logger, or a console-based logger whose
266
+ * debug output is gated by the `debug` option (defaulting to on
267
+ * outside production)
268
+ */
269
+ resolveLogger(options) {
270
+ return options.logger ?? new ConsoleLogger(options.debug ?? process.env.NODE_ENV !== "production");
271
+ }
272
+ /**
273
+ * Builds a circuit breaker if `circuitBreaker` is truthy on the
274
+ * options. Passing `true` uses default thresholds, passing an options
275
+ * object tunes them. Returns undefined when the breaker is disabled
276
+ */
277
+ resolveCircuitBreaker(options) {
278
+ if (!options.circuitBreaker) return void 0;
279
+ return new CircuitBreaker(options.circuitBreaker === true ? void 0 : options.circuitBreaker);
280
+ }
281
+ /**
282
+ * Makes a single logical LLM call, transparently retrying on failure
283
+ * according to the configured retry policy
284
+ *
285
+ * Fails fast if the circuit breaker is open or the signal is already
286
+ * aborted, before any request is dispatched. On exhausting all
287
+ * retries, records a circuit breaker failure and rejects with a
288
+ * normalized LLMError
289
+ *
290
+ * @param params : System/user content plus per call overrides
291
+ * (model, temperature, jsonMode, schema, signal, etc)
292
+ * @returns The parsed and optionally schema-validated response, or
293
+ * the raw string content when jsonMode is disabled
294
+ */
295
+ async call(params) {
296
+ this.breaker?.assertClosed();
297
+ if (params.signal?.aborted) throw new LLMError("LLM request aborted", "aborted");
298
+ const requestId = params.requestId ?? (0, crypto.randomUUID)();
299
+ try {
300
+ const result = await this.retryWithBackoff(() => this.executeCall(params, requestId), requestId, params.signal);
301
+ return result;
302
+ } catch (error) {
303
+ this.breaker?.recordFailure();
304
+ throw this.normalizeError(error, params.signal);
305
+ }
306
+ }
307
+ /**
308
+ * Runs `fn`, retrying with backoff according to `shouldRetry` policy
309
+ * Purely mechanical: knows nothing about LLM specifics beyond the retry
310
+ * predicate, so its testable independent of request/response shaping
311
+ */
312
+ async retryWithBackoff(fn, requestId, signal) {
313
+ let lastError;
314
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) try {
315
+ if (attempt > 0) await this.recoverDelay(requestId, attempt, signal);
316
+ return await fn();
317
+ } catch (error) {
318
+ lastError = error;
319
+ if (!this.shouldRetry(error, signal)) break;
320
+ }
321
+ throw lastError;
322
+ }
323
+ /**
324
+ * Converts any thrown value into a well-typed LLMError for the public
325
+ * API surface. Preserves an existing LLMError as is, reports aborted
326
+ * signals as such, classifies errors carrying an http status as
327
+ * type api, and otherwise falls back to a generic unknown error.
328
+ */
329
+ normalizeError(error, signal) {
330
+ if (signal?.aborted) return new LLMError("LLM request aborted", "aborted");
331
+ if (error instanceof LLMError) return error;
332
+ const status = extractStatus(error);
333
+ if (status !== void 0) return new LLMError("LLM request failed", "api", status);
334
+ return new LLMError("LLM request failed", "unknown");
335
+ }
336
+ /**
337
+ * Performs a single attempt: builds the request, dispatches it with a
338
+ * timeout, and shapes the response. Throws on an empty response so
339
+ * the retry loop treats it like any other transient failure. Records
340
+ * usage and a circuit breaker success before returning
341
+ */
342
+ async executeCall(params, requestId) {
343
+ const { useJson, model, request } = this.buildRequestPayload(params);
344
+ const response = await withTimeout((attemptSignal) => this.client.chat.completions.create(request, { signal: attemptSignal }), this.timeoutMs, params.signal);
345
+ const content = response.choices?.[0]?.message?.content?.trim();
346
+ if (!content) throw new LLMError("Empty LLM response", "api");
347
+ this.logger.debug(`[vern:${requestId}] output:\n${content.slice(0, 800)}`);
348
+ this.recordUsage(response, requestId, model);
349
+ this.breaker?.recordSuccess();
350
+ if (!useJson) return content;
351
+ return this.parseAndValidate(content, params.schema);
352
+ }
353
+ /**
354
+ * Applies per call defaults and shapes the params into the request
355
+ * object expected by the underlying client, including the resolved
356
+ * response format. Also returns whether JSON parsing should be
357
+ * applied to the response and which model was ultimately used
358
+ */
359
+ buildRequestPayload(params) {
360
+ const { systemPrompt, userContent, temperature = .2, jsonMode = true, maxTokens = this.defaultMaxTokens, model = this.model, reasoningEffort, jsonSchema } = params;
361
+ const useJson = jsonMode || Boolean(jsonSchema);
362
+ const responseFormat = this.buildResponseFormat(jsonSchema, useJson);
363
+ const request = {
364
+ model,
365
+ temperature,
366
+ max_tokens: maxTokens,
367
+ ...responseFormat ? { response_format: responseFormat } : {},
368
+ ...reasoningEffort ? { reasoning_effort: reasoningEffort } : {},
369
+ messages: [{
370
+ role: "system",
371
+ content: systemPrompt
372
+ }, {
373
+ role: "user",
374
+ content: userContent
375
+ }]
376
+ };
377
+ return {
378
+ useJson,
379
+ model,
380
+ request
381
+ };
382
+ }
383
+ /**
384
+ * Chooses the response format to send to the provider. A provider
385
+ * native json schema takes priority when supplied, constraining
386
+ * generation directly, otherwise falls back to the looser json
387
+ * object mode when JSON output is requested, or no format at all
388
+ * for plain text responses
389
+ */
390
+ buildResponseFormat(jsonSchema, useJson) {
391
+ if (jsonSchema) return {
392
+ type: "json_schema",
393
+ json_schema: {
394
+ name: jsonSchema.name,
395
+ schema: jsonSchema.schema,
396
+ strict: jsonSchema.strict ?? true,
397
+ description: jsonSchema.description
398
+ }
399
+ };
400
+ return useJson ? { type: "json_object" } : void 0;
401
+ }
402
+ /**
403
+ * Reports token usage to the caller supplied onUsage callback, when
404
+ * both a callback was configured and the provider actually returned
405
+ * usage data on this response. A no op otherwise.
406
+ */
407
+ recordUsage(response, requestId, model) {
408
+ if (!response.usage || !this.onUsage) return;
409
+ this.onUsage({
410
+ promptTokens: response.usage.prompt_tokens ?? 0,
411
+ completionTokens: response.usage.completion_tokens ?? 0,
412
+ totalTokens: response.usage.total_tokens ?? 0,
413
+ requestId,
414
+ model
415
+ });
416
+ }
417
+ /**
418
+ * Parses the raw response content as JSON and, when a schema is
419
+ * supplied, validates the parsed value against it. Throws a parse
420
+ * type LLMError on malformed JSON and a validation type LLMError,
421
+ * carrying the schemas issues, on a failed validation
422
+ */
423
+ parseAndValidate(content, schema) {
424
+ const parsed = this.parseJson(content);
425
+ if (parsed == null) throw new LLMError("Invalid JSON response", "parse");
426
+ if (!schema) return parsed;
427
+ const result = schema.safeParse(parsed);
428
+ if (!result.success) throw new LLMError("Schema validation failed", "validation", void 0, result.error);
429
+ return result.data;
430
+ }
431
+ /**
432
+ * Waits out the backoff delay for a given retry attempt, logging the
433
+ * attempt for observability before the wait begins. Rejects early if
434
+ * the signal aborts during the wait
435
+ */
436
+ async recoverDelay(requestId, attempt, signal) {
437
+ const delay = getBackoffDelay(this.baseDelayMs, attempt);
438
+ this.logger.warn(`[vern:${requestId}] recovery attempt ${attempt}/${this.maxRetries}, waiting ${delay}ms`);
439
+ await waitForRetry(delay, signal);
440
+ }
441
+ /**
442
+ * Decides whether a failed attempt is worth retrying. Never retries
443
+ * once the signal has aborted, never retries a parse or validation
444
+ * failure since those stem from the response content rather than a
445
+ * transient fault, and never retries a status code the caller has
446
+ * marked as non retryable. Retries everything else
447
+ */
448
+ shouldRetry(error, signal) {
449
+ if (signal?.aborted) return false;
450
+ if (error instanceof LLMError && (error.type === "parse" || error.type === "validation")) return false;
451
+ const status = extractStatus(error);
452
+ if (status !== void 0 && this.nonRetryableStatus.includes(status)) return false;
453
+ return true;
454
+ }
455
+ /**
456
+ * Thin cache wrapper around caller supplied logic. `params.fn` is expected
457
+ * to be a call that itself invokes `this.call(...)` (see `cachedLLMCall`
458
+ * below for a convenience wrapper that wires this up automatically),
459
+ * `cachedCall` does not itself apply retry/timeout policy.
460
+ */
461
+ async cachedCall(params) {
462
+ const cached = await this.cache.get(params.cacheKey);
463
+ if (cached != null) return cached;
464
+ try {
465
+ await params.reserveUsage?.();
466
+ const result = await params.fn();
467
+ try {
468
+ await this.cache.set(params.cacheKey, result, params.ttl);
469
+ } catch (error) {
470
+ this.logger.error("[VernLLM] cache write failed", { message: error instanceof Error ? error.message : "unknown" });
471
+ }
472
+ return result;
473
+ } catch (error) {
474
+ try {
475
+ await params.refundUsage?.();
476
+ } catch (refundError) {
477
+ this.logger.error("[VernLLM] refundUsage failed", { message: refundError instanceof Error ? refundError.message : "unknown" });
478
+ }
479
+ throw error;
480
+ }
481
+ }
482
+ /**
483
+ * Convenience wrapper composing `call` + `cachedCall`, so cached LLM calls
484
+ * automatically get retry/timeout/circuit-breaker behavior without callers
485
+ * having to remember to wire `fn: () => this.call(...)` themselves
486
+ */
487
+ async cachedLLMCall(params) {
488
+ const { call: callParams,...cacheParams } = params;
489
+ return this.cachedCall({
490
+ ...cacheParams,
491
+ fn: () => this.call(callParams)
492
+ });
493
+ }
494
+ /**
495
+ * Returns the current circuit breaker state, or undefined when no
496
+ * circuit breaker was configured on this instance
497
+ */
498
+ getCircuitState() {
499
+ return this.breaker?.getState();
500
+ }
501
+ };
502
+
503
+ //#endregion
504
+ //#region src/adapters/anthropic.ts
505
+ /**
506
+ * Wraps an Anthropic SDK client so it satisfies the same `LLMClient`
507
+ * interface VernLLM uses for OpenAI/Groq. Anthropics Messages API has no
508
+ * `response_format: json_object` equivalent, so when the caller requests
509
+ * JSON mode, this adapter appends an instruction to the system prompt
510
+ * asking the model to respond with JSON only
511
+ */
512
+ function fromAnthropic(anthropicClient) {
513
+ return { chat: { completions: { async create(params, options) {
514
+ const systemMessage = params.messages.find((m) => m.role === "system");
515
+ const userMessages = params.messages.filter((m) => m.role === "user");
516
+ let jsonInstruction;
517
+ if (params.response_format?.type === "json_schema") {
518
+ const { name, schema } = params.response_format.json_schema;
519
+ jsonInstruction = `Respond with valid JSON only, no prose or markdown fences. The JSON must conform to this schema (name: "${name}"):\n${JSON.stringify(schema)}`;
520
+ } else if (params.response_format?.type === "json_object") jsonInstruction = "Respond with valid JSON only, no prose or markdown fences.";
521
+ const system = [systemMessage?.content, jsonInstruction].filter(Boolean).join("\n\n");
522
+ const response = await anthropicClient.messages.create({
523
+ model: params.model,
524
+ max_tokens: params.max_tokens,
525
+ temperature: params.temperature,
526
+ system: system || void 0,
527
+ messages: userMessages.map((m) => ({
528
+ role: "user",
529
+ content: m.content
530
+ }))
531
+ }, options);
532
+ const text = response.content.find((block) => block.type === "text")?.text ?? "";
533
+ return {
534
+ choices: [{ message: { content: text } }],
535
+ usage: {
536
+ prompt_tokens: response.usage?.input_tokens,
537
+ completion_tokens: response.usage?.output_tokens,
538
+ total_tokens: (response.usage?.input_tokens ?? 0) + (response.usage?.output_tokens ?? 0)
539
+ }
540
+ };
541
+ } } } };
542
+ }
543
+
544
+ //#endregion
545
+ //#region src/adapters/gemini.ts
546
+ /**
547
+ * Wraps a Gemini client so it satisfies the `LLMClient` interface VernLLM
548
+ * uses for OpenAI/Groq. Geminis shape differs on nearly every axis: a
549
+ * `contents` array instead of `messages`, a separate `systemInstruction`
550
+ * field instead of a `system` role message, `generationConfig` instead of
551
+ * top-level `temperature`/`max_tokens`, and native JSON Schema support via
552
+ * `responseMimeType: 'application/json'` + `responseSchema` (so `jsonSchema`
553
+ * is provider-enforced here, unlike the Anthropic adapters prompt-embedding
554
+ * fallback). `reasoning_effort` has no equivalent. Geminis thinking models
555
+ * use a token budget, not an effort tier, so its dropped, same as Anthropic.
556
+ */
557
+ function fromGemini(geminiClient) {
558
+ return { chat: { completions: { async create(params, options) {
559
+ const systemMessage = params.messages.find((m) => m.role === "system");
560
+ const userMessages = params.messages.filter((m) => m.role === "user");
561
+ const wantsJson = Boolean(params.response_format);
562
+ const generationConfig = {
563
+ temperature: params.temperature,
564
+ maxOutputTokens: params.max_tokens
565
+ };
566
+ if (wantsJson) generationConfig.responseMimeType = "application/json";
567
+ if (params.response_format?.type === "json_schema") generationConfig.responseSchema = params.response_format.json_schema.schema;
568
+ const response = await geminiClient.generateContent({
569
+ model: params.model,
570
+ contents: userMessages.map((m) => ({
571
+ role: "user",
572
+ parts: [{ text: m.content }]
573
+ })),
574
+ systemInstruction: systemMessage ? { parts: [{ text: systemMessage.content }] } : void 0,
575
+ generationConfig
576
+ }, options);
577
+ const text = response.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("") ?? "";
578
+ return {
579
+ choices: [{ message: { content: text } }],
580
+ usage: {
581
+ prompt_tokens: response.usageMetadata?.promptTokenCount,
582
+ completion_tokens: response.usageMetadata?.candidatesTokenCount,
583
+ total_tokens: response.usageMetadata?.totalTokenCount
584
+ }
585
+ };
586
+ } } } };
587
+ }
588
+
589
+ //#endregion
590
+ //#region src/adapters/bedrock.ts
591
+ /**
592
+ * Wraps a Bedrock Converse-API client so it satisfies the `LLMClient`
593
+ * interface VernLLM uses for OpenAI/Groq. The Converse API is unified
594
+ * across Bedrocks model families (Anthropic, Titan, Llama, Mistral, etc.),
595
+ * so unlike raw per-model Bedrock invocation, this one adapter works
596
+ * regardless of which underlying model `modelId` points at, as long as
597
+ * that model supports Converse (most current-generation ones do)
598
+ *
599
+ * Theres no uniform native JSON Schema enforcement across families here
600
+ * (some support it via forced tool-use, which varies per model), so
601
+ * `jsonSchema`/`jsonMode` are emulated via a system-prompt instruction, same
602
+ * approach as the Anthropic adapter. `reasoning_effort` has no Converse
603
+ * equivalent and is dropped
604
+ */
605
+ function fromBedrock(bedrockClient) {
606
+ return { chat: { completions: { async create(params, options) {
607
+ const systemMessage = params.messages.find((m) => m.role === "system");
608
+ const userMessages = params.messages.filter((m) => m.role === "user");
609
+ let jsonInstruction;
610
+ if (params.response_format?.type === "json_schema") {
611
+ const { name, schema } = params.response_format.json_schema;
612
+ jsonInstruction = `Respond with valid JSON only, no prose or markdown fences. The JSON must conform to this schema (name: "${name}"):\n${JSON.stringify(schema)}`;
613
+ } else if (params.response_format?.type === "json_object") jsonInstruction = "Respond with valid JSON only, no prose or markdown fences.";
614
+ const systemParts = [systemMessage?.content, jsonInstruction].filter((s) => Boolean(s));
615
+ const response = await bedrockClient.converse({
616
+ modelId: params.model,
617
+ messages: userMessages.map((m) => ({
618
+ role: "user",
619
+ content: [{ text: m.content }]
620
+ })),
621
+ system: systemParts.length ? systemParts.map((text$1) => ({ text: text$1 })) : void 0,
622
+ inferenceConfig: {
623
+ temperature: params.temperature,
624
+ maxTokens: params.max_tokens
625
+ }
626
+ }, options);
627
+ const text = response.output?.message?.content?.map((c) => c.text ?? "").join("") ?? "";
628
+ return {
629
+ choices: [{ message: { content: text } }],
630
+ usage: {
631
+ prompt_tokens: response.usage?.inputTokens,
632
+ completion_tokens: response.usage?.outputTokens,
633
+ total_tokens: response.usage?.totalTokens
634
+ }
635
+ };
636
+ } } } };
637
+ }
638
+
639
+ //#endregion
640
+ //#region src/adapters/fetch.ts
641
+ /**
642
+ * A fetch-based escape hatch for providers with no SDK, or where pulling one
643
+ * in isnt worth it. You supply the URL, headers, and two small mapping
644
+ * functions; this handles the HTTP call and slots the result into the same
645
+ * `LLMClient` shape every other adapter produces, so retries, timeouts,
646
+ * the circuit breaker, and JSON/schema handling all still work unmodified
647
+ *
648
+ * Non-2xx responses throw an error with `.status` set to the HTTP status
649
+ * code, so VernLLMs `nonRetryableStatus` handling (e.g. failing fast on
650
+ * 401/403) applies here too
651
+ */
652
+ function fromFetch(config) {
653
+ return { chat: { completions: { async create(params, options) {
654
+ const url = typeof config.url === "function" ? config.url(params) : config.url;
655
+ const headers = typeof config.headers === "function" ? await config.headers() : config.headers;
656
+ const res = await fetch(url, {
657
+ method: config.method ?? "POST",
658
+ headers: {
659
+ "Content-Type": "application/json",
660
+ ...headers
661
+ },
662
+ body: JSON.stringify(config.mapRequest(params)),
663
+ signal: options.signal
664
+ });
665
+ if (!res.ok) {
666
+ const body = await res.text().catch(() => "");
667
+ const err = new Error(`Fetch adapter request failed (${res.status}): ${body.slice(0, 500)}`);
668
+ err.status = res.status;
669
+ throw err;
670
+ }
671
+ const json = await res.json();
672
+ const { content, usage } = config.mapResponse(json);
673
+ return {
674
+ choices: [{ message: { content } }],
675
+ usage: usage ? {
676
+ prompt_tokens: usage.promptTokens,
677
+ completion_tokens: usage.completionTokens,
678
+ total_tokens: usage.totalTokens
679
+ } : void 0
680
+ };
681
+ } } } };
682
+ }
683
+
684
+ //#endregion
685
+ //#region src/adapters/openaiCompatible.ts
686
+ /**
687
+ * Passthrough adapter for any SDK/client whose `chat.completions.create`
688
+ * already matches the OpenAI wire format 1:1 : this covers most hosted
689
+ * inference providers, since "OpenAI-compatible" is a de facto standard for
690
+ * chat completion APIs. No transformation happens here, this exists purely
691
+ * so call sites read clearly (`fromMistral(client)` vs handing a Mistral
692
+ * client to something typed for OpenAI) and so a real transformation could
693
+ * be added later, per-provider, without a breaking change.
694
+ *
695
+ * Not every SDKs own TypeScript types line up exactly with `LLMClient`
696
+ * (extra fields, stricter unions, etc.), so this takes `unknown` and casts:
697
+ * the actual compatibility contract is the JSON each provider sends and
698
+ * receives over the wire, not the SDKs TS types.
699
+ */
700
+ function fromOpenAICompatible(client) {
701
+ return client;
702
+ }
703
+ /** Groqs SDK matches the OpenAI wire format */
704
+ const fromGroq = fromOpenAICompatible;
705
+ /** Mistrals `chat.completions`-shaped client (or their OpenAI-compat endpoint) */
706
+ const fromMistral = fromOpenAICompatible;
707
+ /** DeepSeeks API is OpenAI-compatible */
708
+ const fromDeepSeek = fromOpenAICompatible;
709
+ /** Cerebras inference API is OpenAI-compatible */
710
+ const fromCerebras = fromOpenAICompatible;
711
+ /** Together AIs API is OpenAI-compatible */
712
+ const fromTogether = fromOpenAICompatible;
713
+ /** Fireworks AIs API is OpenAI-compatible */
714
+ const fromFireworks = fromOpenAICompatible;
715
+ /**
716
+ * Ollama exposes an OpenAI-compatible endpoint at `/v1/chat/completions`
717
+ * (as opposed to its native `/api/chat` format, which differs). Point an
718
+ * OpenAI SDK instances `baseURL` at your Ollama server and pass it here:
719
+ * this does not talk to Ollamas native API directly.
720
+ */
721
+ const fromOllama = fromOpenAICompatible;
722
+
723
+ //#endregion
724
+ exports.CircuitBreaker = CircuitBreaker
725
+ exports.ConsoleLogger = ConsoleLogger
726
+ exports.InMemoryCacheAdapter = InMemoryCacheAdapter
727
+ exports.LLMError = LLMError
728
+ exports.VernLLM = VernLLM
729
+ exports.fromAnthropic = fromAnthropic
730
+ exports.fromBedrock = fromBedrock
731
+ exports.fromCerebras = fromCerebras
732
+ exports.fromDeepSeek = fromDeepSeek
733
+ exports.fromFetch = fromFetch
734
+ exports.fromFireworks = fromFireworks
735
+ exports.fromGemini = fromGemini
736
+ exports.fromGroq = fromGroq
737
+ exports.fromMistral = fromMistral
738
+ exports.fromOllama = fromOllama
739
+ exports.fromOpenAICompatible = fromOpenAICompatible
740
+ exports.fromTogether = fromTogether
741
+ exports.isLLMError = isLLMError
742
+ //# sourceMappingURL=index.cjs.map