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