@agentskit/adapters 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,11 +2,21 @@
2
2
 
3
3
  Connect to any LLM provider — and swap between them — without touching your app code.
4
4
 
5
- ## Why
5
+ [![npm version](https://img.shields.io/npm/v/@agentskit/adapters?color=blue)](https://www.npmjs.com/package/@agentskit/adapters)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@agentskit/adapters)](https://www.npmjs.com/package/@agentskit/adapters)
7
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@agentskit/adapters)](https://bundlephobia.com/package/@agentskit/adapters)
8
+ [![license](https://img.shields.io/npm/l/@agentskit/adapters)](../../LICENSE)
9
+ [![stability](https://img.shields.io/badge/stability-stable-brightgreen)](../../docs/STABILITY.md)
10
+ [![GitHub stars](https://img.shields.io/github/stars/EmersonBraun/agentskit?style=social)](https://github.com/EmersonBraun/agentskit)
11
+
12
+ **Tags:** `ai` · `agents` · `llm` · `agentskit` · `openai` · `anthropic` · `claude` · `gemini` · `chatgpt` · `ollama` · `embeddings` · `providers`
13
+
14
+ ## Why adapters
6
15
 
7
16
  - **Vendor independence** — switch from OpenAI to Anthropic to a local Ollama model by changing one line; your hooks, runtime, and tools stay untouched
8
17
  - **10+ providers included** — Anthropic, OpenAI, Gemini, Ollama, DeepSeek, Grok, Kimi, LangChain, Vercel AI SDK, and any raw `ReadableStream`
9
18
  - **Embedder functions built in** — the same adapter pattern covers text embeddings, so you can reuse provider config for both chat and RAG
19
+ - **One-line local AI** — `ollama({ model: 'llama3.1' })` for fully offline agents with no API key required
10
20
 
11
21
  ## Install
12
22
 
@@ -45,9 +55,12 @@ const rag = createRAG({
45
55
  })
46
56
  ```
47
57
 
48
- ## Next steps
58
+ ## Features
49
59
 
50
- - Pair adapters with [`@agentskit/runtime`](https://www.npmjs.com/package/@agentskit/runtime), [`@agentskit/react`](https://www.npmjs.com/package/@agentskit/react), or [`@agentskit/ink`](https://www.npmjs.com/package/@agentskit/ink) — the adapter instance is the only provider-specific piece
60
+ - Providers: Anthropic, OpenAI, Gemini, Ollama, DeepSeek, Grok, Kimi, LangChain, LangGraph, Vercel AI SDK, generic `ReadableStream`
61
+ - Embedders: `openaiEmbedder`, `geminiEmbedder`, `ollamaEmbedder`
62
+ - All adapters satisfy `Adapter` contract v1 (ADR 0001) — substitutable anywhere in the ecosystem
63
+ - Custom adapter authoring via `createAdapter()`
51
64
 
52
65
  ## Ecosystem
53
66
 
@@ -58,6 +71,16 @@ const rag = createRAG({
58
71
  | [@agentskit/rag](https://www.npmjs.com/package/@agentskit/rag) | `createRAG` + embedders |
59
72
  | [@agentskit/memory](https://www.npmjs.com/package/@agentskit/memory) | Vector + chat memory backends |
60
73
 
74
+ ## Contributors
75
+
76
+ <a href="https://github.com/EmersonBraun/agentskit/graphs/contributors">
77
+ <img src="https://contrib.rocks/image?repo=EmersonBraun/agentskit" alt="AgentsKit contributors" />
78
+ </a>
79
+
80
+ ## License
81
+
82
+ MIT — see [LICENSE](../../LICENSE).
83
+
61
84
  ## Docs
62
85
 
63
- [Full documentation](https://emersonbraun.github.io/agentskit/)
86
+ [Full documentation](https://www.agentskit.io) · [GitHub](https://github.com/EmersonBraun/agentskit)
package/dist/index.cjs CHANGED
@@ -255,12 +255,73 @@ async function* parseOllamaStream(stream) {
255
255
  }
256
256
  yield { type: "done" };
257
257
  }
258
- function createStreamSource(doFetch, parse, errorLabel) {
258
+ var DEFAULT_RETRY = {
259
+ maxAttempts: 3,
260
+ baseDelayMs: 500,
261
+ maxDelayMs: 8e3,
262
+ jitter: true,
263
+ retryOn: ({ error, response }) => {
264
+ if (response) {
265
+ return [408, 429, 500, 502, 503, 504].includes(response.status);
266
+ }
267
+ if (error instanceof DOMException && error.name === "AbortError") return false;
268
+ return true;
269
+ }
270
+ };
271
+ function defaultSleep(ms) {
272
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
273
+ }
274
+ function computeDelay(attempt, opts) {
275
+ const exp = Math.min(opts.maxDelayMs, opts.baseDelayMs * Math.pow(2, attempt - 1));
276
+ if (!opts.jitter) return exp;
277
+ return Math.floor(Math.random() * exp);
278
+ }
279
+ function parseRetryAfter(value) {
280
+ if (!value) return void 0;
281
+ const n = Number(value);
282
+ if (!Number.isNaN(n)) return n * 1e3;
283
+ const date = Date.parse(value);
284
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
285
+ return void 0;
286
+ }
287
+ async function fetchWithRetry(doFetch, signal, retryOpt = {}) {
288
+ const opts = {
289
+ ...DEFAULT_RETRY,
290
+ ...retryOpt
291
+ };
292
+ const sleep = retryOpt.sleep ?? defaultSleep;
293
+ let lastError;
294
+ for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
295
+ if (signal.aborted) throw new DOMException("Aborted", "AbortError");
296
+ try {
297
+ const response = await doFetch(signal);
298
+ if (response.ok) return response;
299
+ if (attempt >= opts.maxAttempts || !opts.retryOn({ response, attempt })) {
300
+ return response;
301
+ }
302
+ const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
303
+ const delay = retryAfterMs ?? computeDelay(attempt, opts);
304
+ retryOpt.onRetry?.({ attempt, delayMs: delay, reason: `HTTP ${response.status}` });
305
+ await sleep(delay);
306
+ response.body?.cancel().catch(() => {
307
+ });
308
+ } catch (err) {
309
+ lastError = err;
310
+ if (err instanceof DOMException && err.name === "AbortError") throw err;
311
+ if (attempt >= opts.maxAttempts || !opts.retryOn({ error: err, attempt })) throw err;
312
+ const delay = computeDelay(attempt, opts);
313
+ retryOpt.onRetry?.({ attempt, delayMs: delay, reason: err.message });
314
+ await sleep(delay);
315
+ }
316
+ }
317
+ throw lastError ?? new Error("fetchWithRetry: exhausted attempts");
318
+ }
319
+ function createStreamSource(doFetch, parse, errorLabel, retry) {
259
320
  let abortController = new AbortController();
260
321
  return {
261
322
  stream: async function* () {
262
323
  try {
263
- const response = await doFetch(abortController.signal);
324
+ const response = await fetchWithRetry(doFetch, abortController.signal, retry ?? {});
264
325
  if (!response.ok || !response.body) {
265
326
  yield { type: "error", content: `${errorLabel} error: ${response.status}` };
266
327
  return;
@@ -280,11 +341,78 @@ function createStreamSource(doFetch, parse, errorLabel) {
280
341
  }
281
342
  };
282
343
  }
344
+ function chunkText(text, targetSize = 32) {
345
+ if (text.length <= targetSize) return [text];
346
+ const out = [];
347
+ let i = 0;
348
+ while (i < text.length) {
349
+ let end = Math.min(text.length, i + targetSize);
350
+ if (end < text.length) {
351
+ const nextSpace = text.indexOf(" ", end);
352
+ if (nextSpace !== -1 && nextSpace - end <= 8) end = nextSpace + 1;
353
+ }
354
+ out.push(text.slice(i, end));
355
+ i = end;
356
+ }
357
+ return out;
358
+ }
359
+ function simulateStream(doFetch, extractText, errorLabel, options = {}) {
360
+ const { chunkSize = 32, delayMs = 8, retry } = options;
361
+ let abortController = new AbortController();
362
+ return {
363
+ stream: async function* () {
364
+ try {
365
+ const response = await fetchWithRetry(doFetch, abortController.signal, retry ?? {});
366
+ if (!response.ok) {
367
+ yield { type: "error", content: `${errorLabel} error: ${response.status}` };
368
+ return;
369
+ }
370
+ const text = await extractText(response);
371
+ const chunks = chunkText(text, chunkSize);
372
+ for (const chunk of chunks) {
373
+ if (abortController === null) return;
374
+ if (delayMs > 0) {
375
+ await new Promise((resolve2, reject) => {
376
+ const timer = setTimeout(resolve2, delayMs);
377
+ abortController?.signal.addEventListener(
378
+ "abort",
379
+ () => {
380
+ clearTimeout(timer);
381
+ reject(new DOMException("Aborted", "AbortError"));
382
+ },
383
+ { once: true }
384
+ );
385
+ });
386
+ }
387
+ yield { type: "text", content: chunk };
388
+ }
389
+ yield { type: "done" };
390
+ } catch (err) {
391
+ if (err instanceof DOMException && err.name === "AbortError") return;
392
+ yield {
393
+ type: "error",
394
+ content: err instanceof Error ? err.message : String(err)
395
+ };
396
+ }
397
+ },
398
+ abort: () => {
399
+ abortController?.abort();
400
+ abortController = null;
401
+ }
402
+ };
403
+ }
283
404
 
284
405
  // src/anthropic.ts
285
406
  function anthropic(config) {
286
- const { apiKey, model, baseUrl = "https://api.anthropic.com", maxTokens = 4096 } = config;
407
+ const { apiKey, model, baseUrl = "https://api.anthropic.com", maxTokens = 4096, retry } = config;
287
408
  return {
409
+ capabilities: {
410
+ streaming: true,
411
+ tools: true,
412
+ reasoning: model.includes("sonnet") || model.includes("opus"),
413
+ multiModal: true,
414
+ usage: true
415
+ },
288
416
  createSource: (request) => {
289
417
  const body = {
290
418
  model,
@@ -310,7 +438,8 @@ function anthropic(config) {
310
438
  signal
311
439
  }),
312
440
  parseAnthropicStream,
313
- "Anthropic API"
441
+ "Anthropic API",
442
+ retry
314
443
  );
315
444
  }
316
445
  };
@@ -318,8 +447,17 @@ function anthropic(config) {
318
447
 
319
448
  // src/openai.ts
320
449
  function openai(config) {
321
- const { apiKey, model, baseUrl = "https://api.openai.com" } = config;
450
+ const { apiKey, model, baseUrl = "https://api.openai.com", retry } = config;
322
451
  return {
452
+ capabilities: {
453
+ streaming: true,
454
+ tools: true,
455
+ // o1 / o3 models emit reasoning; older models don't. Accurate per-model
456
+ // detection would need a model registry; 'true' is the safer default here.
457
+ reasoning: model.startsWith("o1") || model.startsWith("o3"),
458
+ multiModal: model.startsWith("gpt-4") || model.startsWith("o"),
459
+ usage: true
460
+ },
323
461
  createSource: (request) => {
324
462
  const body = {
325
463
  model,
@@ -347,7 +485,8 @@ function openai(config) {
347
485
  signal
348
486
  }),
349
487
  parseOpenAIStream,
350
- "OpenAI API"
488
+ "OpenAI API",
489
+ retry
351
490
  );
352
491
  }
353
492
  };
@@ -355,8 +494,14 @@ function openai(config) {
355
494
 
356
495
  // src/gemini.ts
357
496
  function gemini(config) {
358
- const { apiKey, model, baseUrl = "https://generativelanguage.googleapis.com" } = config;
497
+ const { apiKey, model, baseUrl = "https://generativelanguage.googleapis.com", retry } = config;
359
498
  return {
499
+ capabilities: {
500
+ streaming: true,
501
+ tools: true,
502
+ multiModal: true,
503
+ usage: true
504
+ },
360
505
  createSource: (request) => {
361
506
  const body = {
362
507
  contents: request.messages.filter((message) => message.role !== "system").map((message) => ({
@@ -379,7 +524,8 @@ function gemini(config) {
379
524
  }
380
525
  ),
381
526
  parseGeminiStream,
382
- "Gemini API"
527
+ "Gemini API",
528
+ retry
383
529
  );
384
530
  }
385
531
  };
@@ -400,8 +546,14 @@ var grok = createOpenAICompatibleAdapter("https://api.x.ai");
400
546
 
401
547
  // src/ollama.ts
402
548
  function ollama(config) {
403
- const { model, baseUrl = "http://localhost:11434" } = config;
549
+ const { model, baseUrl = "http://localhost:11434", retry } = config;
404
550
  return {
551
+ capabilities: {
552
+ streaming: true,
553
+ tools: false,
554
+ // varies by model; default 'false' — enable via capabilities.extensions if your model supports it
555
+ multiModal: model.includes("llava") || model.includes("vision")
556
+ },
405
557
  createSource: (request) => {
406
558
  const body = {
407
559
  model,
@@ -419,7 +571,8 @@ function ollama(config) {
419
571
  signal
420
572
  }),
421
573
  parseOllamaStream,
422
- "Ollama API"
574
+ "Ollama API",
575
+ retry
423
576
  );
424
577
  }
425
578
  };
@@ -516,8 +669,12 @@ async function* parseVercelStream(stream) {
516
669
  yield { type: "done" };
517
670
  }
518
671
  function vercelAI(config) {
519
- const { api, headers = {} } = config;
672
+ const { api, headers = {}, retry } = config;
520
673
  return {
674
+ capabilities: {
675
+ // Vercel AI routes can do any of these — whether they do depends on
676
+ // your route handler. Omit the field instead of lying.
677
+ },
521
678
  createSource: (request) => {
522
679
  const body = {
523
680
  messages: request.messages.map((message) => ({ role: message.role, content: message.content })),
@@ -532,12 +689,105 @@ function vercelAI(config) {
532
689
  signal
533
690
  }),
534
691
  parseVercelStream,
535
- "API"
692
+ "API",
693
+ retry
536
694
  );
537
695
  }
538
696
  };
539
697
  }
540
698
 
699
+ // src/mock.ts
700
+ function mockAdapter(options) {
701
+ const { response, delayMs = 0, history } = options;
702
+ let callIndex = 0;
703
+ return {
704
+ capabilities: {
705
+ streaming: true,
706
+ tools: true,
707
+ reasoning: true,
708
+ multiModal: true,
709
+ usage: true
710
+ },
711
+ createSource: (request) => {
712
+ history?.push(request);
713
+ const myCall = callIndex++;
714
+ let cancelled = false;
715
+ return {
716
+ stream: async function* () {
717
+ const chunks = resolve(response, myCall, request);
718
+ let endedExplicitly = false;
719
+ for (const chunk of chunks) {
720
+ if (cancelled) return;
721
+ if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
722
+ yield chunk;
723
+ if (chunk.type === "done" || chunk.type === "error") {
724
+ endedExplicitly = true;
725
+ }
726
+ }
727
+ if (!endedExplicitly) yield { type: "done" };
728
+ },
729
+ abort: () => {
730
+ cancelled = true;
731
+ }
732
+ };
733
+ }
734
+ };
735
+ }
736
+ function resolve(response, callIndex, request) {
737
+ if (Array.isArray(response) && response.length > 0 && Array.isArray(response[0])) {
738
+ const sequence = response;
739
+ const item = sequence[callIndex % sequence.length];
740
+ return item;
741
+ }
742
+ if (Array.isArray(response) && response.length > 0 && typeof response[0] === "function") {
743
+ const sequence = response;
744
+ return sequence[callIndex % sequence.length](request);
745
+ }
746
+ if (typeof response === "function") {
747
+ return response(request);
748
+ }
749
+ return response;
750
+ }
751
+ function recordingAdapter(inner, sink) {
752
+ return {
753
+ createSource: (request) => {
754
+ const innerSource = inner.createSource(request);
755
+ const captured = [];
756
+ const recordedAt = (/* @__PURE__ */ new Date()).toISOString();
757
+ return {
758
+ stream: async function* () {
759
+ try {
760
+ for await (const chunk of innerSource.stream()) {
761
+ captured.push(chunk);
762
+ yield chunk;
763
+ }
764
+ } finally {
765
+ await sink.push({ recordedAt, request, chunks: captured });
766
+ }
767
+ },
768
+ abort: () => innerSource.abort()
769
+ };
770
+ }
771
+ };
772
+ }
773
+ function inMemorySink() {
774
+ const fixture = [];
775
+ return {
776
+ fixture,
777
+ push(turn) {
778
+ fixture.push(turn);
779
+ }
780
+ };
781
+ }
782
+ function replayAdapter(fixture) {
783
+ if (fixture.length === 0) {
784
+ throw new Error("replayAdapter: fixture is empty");
785
+ }
786
+ return mockAdapter({
787
+ response: fixture.map((turn) => turn.chunks)
788
+ });
789
+ }
790
+
541
791
  // src/embedders/openai.ts
542
792
  async function fetchAvailableModels(baseUrl, apiKey) {
543
793
  const response = await fetch(`${baseUrl}/v1/models`, {
@@ -733,23 +983,30 @@ var grokEmbedder = createOpenAICompatibleEmbedder("Grok", "https://api.x.ai");
733
983
  var kimiEmbedder = createOpenAICompatibleEmbedder("Kimi", "https://api.moonshot.ai");
734
984
 
735
985
  exports.anthropic = anthropic;
986
+ exports.chunkText = chunkText;
736
987
  exports.createAdapter = createAdapter;
737
988
  exports.createOpenAICompatibleEmbedder = createOpenAICompatibleEmbedder;
738
989
  exports.deepseek = deepseek;
739
990
  exports.deepseekEmbedder = deepseekEmbedder;
991
+ exports.fetchWithRetry = fetchWithRetry;
740
992
  exports.gemini = gemini;
741
993
  exports.geminiEmbedder = geminiEmbedder;
742
994
  exports.generic = generic;
743
995
  exports.grok = grok;
744
996
  exports.grokEmbedder = grokEmbedder;
997
+ exports.inMemorySink = inMemorySink;
745
998
  exports.kimi = kimi;
746
999
  exports.kimiEmbedder = kimiEmbedder;
747
1000
  exports.langchain = langchain;
748
1001
  exports.langgraph = langgraph;
1002
+ exports.mockAdapter = mockAdapter;
749
1003
  exports.ollama = ollama;
750
1004
  exports.ollamaEmbedder = ollamaEmbedder;
751
1005
  exports.openai = openai;
752
1006
  exports.openaiEmbedder = openaiEmbedder;
1007
+ exports.recordingAdapter = recordingAdapter;
1008
+ exports.replayAdapter = replayAdapter;
1009
+ exports.simulateStream = simulateStream;
753
1010
  exports.vercelAI = vercelAI;
754
1011
  //# sourceMappingURL=index.cjs.map
755
1012
  //# sourceMappingURL=index.cjs.map