ai-lcr 0.6.3 → 0.6.4

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/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ All notable changes to `ai-lcr` are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/), and the project adheres to
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.6.4] — 2026-06-16
8
+
9
+ DX improvements that eliminate per-project boilerplate for consumers.
10
+
11
+ ### Added
12
+
13
+ - **`DEFAULT_PROVIDERS`** — canonical URL + env-var-name for common providers
14
+ (openrouter, deepinfra, tokenmart, deepseek, kunavo, runware, fal). Import
15
+ instead of redeclaring in every app; a URL change propagates on `npm update`.
16
+ - **`createEnvSink(dispatch)`** — reads `LCR_INGEST_URL` / `LCR_PROJECT` /
17
+ `LCR_INGEST_KEY` from env and returns a ready-to-use `onCall` handler (or
18
+ `undefined` when unset). Replaces the identical 30-line `sink.ts` every
19
+ consumer was copy-pasting. Pass `after` from `next/server` as `dispatch`.
20
+ - **`AnyLanguageModel`** — duck-typed model interface on `ProviderEntry` so
21
+ consumers no longer need `as` casts when their `@ai-sdk/provider` version
22
+ differs from ai-lcr's. Runtime behavior unchanged.
23
+
7
24
  ## [0.6.3] — 2026-06-11
8
25
 
9
26
  Caching — both kinds, each off by default and each a pure config flag with no
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ DEFAULT_PROVIDERS: () => DEFAULT_PROVIDERS,
23
24
  DEFAULT_REFERENCE: () => DEFAULT_REFERENCE,
24
25
  MEDIA_PRICING: () => MEDIA_PRICING,
25
26
  MODEL_PRICES: () => MODEL_PRICES,
@@ -29,6 +30,7 @@ __export(index_exports, {
29
30
  classifyError: () => classifyError,
30
31
  classifyErrorKind: () => classifyErrorKind,
31
32
  comparePrices: () => comparePrices,
33
+ createEnvSink: () => createEnvSink,
32
34
  createFalMediaAdapter: () => createFalMediaAdapter,
33
35
  createHttpSink: () => createHttpSink,
34
36
  createKunavoMediaAdapter: () => createKunavoMediaAdapter,
@@ -905,6 +907,30 @@ function createHttpSink(options) {
905
907
  };
906
908
  }
907
909
 
910
+ // src/env-sink.ts
911
+ function createEnvSink(dispatch) {
912
+ const base = process.env.LCR_INGEST_URL?.replace(/\/+$/, "");
913
+ if (!base) return void 0;
914
+ return createHttpSink({
915
+ url: `${base}/api/ingest`,
916
+ headers: process.env.LCR_INGEST_KEY ? { authorization: `Bearer ${process.env.LCR_INGEST_KEY}` } : void 0,
917
+ project: process.env.LCR_PROJECT ?? process.env.SITE_KEY,
918
+ dispatch,
919
+ onError: (err) => console.error("[lcr] ingest POST failed:", err)
920
+ });
921
+ }
922
+
923
+ // src/providers.ts
924
+ var DEFAULT_PROVIDERS = {
925
+ openrouter: { baseURL: "https://openrouter.ai/api/v1", apiKeyEnv: "OPENROUTER_API_KEY" },
926
+ deepinfra: { baseURL: "https://api.deepinfra.com/v1/openai", apiKeyEnv: "DEEPINFRA_API_KEY" },
927
+ tokenmart: { baseURL: "https://model.service-inference.ai/v1", apiKeyEnv: "INFERENCE_API_KEY" },
928
+ deepseek: { baseURL: "https://api.deepseek.com", apiKeyEnv: "DEEPSEEK_API_KEY" },
929
+ kunavo: { baseURL: "https://api.kunavo.com/v1", apiKeyEnv: "KUNAVO_API_KEY" },
930
+ runware: { baseURL: "https://api.runware.ai/v1", apiKeyEnv: "RUNWARE_API_KEY" },
931
+ fal: { baseURL: "https://queue.fal.run", apiKeyEnv: "FAL_KEY" }
932
+ };
933
+
908
934
  // src/text-prices.ts
909
935
  var MODEL_PRICES = {
910
936
  "chatgpt-4o-latest": { input: 5, output: 15 },
@@ -2259,6 +2285,7 @@ function createLCR(config) {
2259
2285
  }
2260
2286
  // Annotate the CommonJS export names for ESM import in node:
2261
2287
  0 && (module.exports = {
2288
+ DEFAULT_PROVIDERS,
2262
2289
  DEFAULT_REFERENCE,
2263
2290
  MEDIA_PRICING,
2264
2291
  MODEL_PRICES,
@@ -2268,6 +2295,7 @@ function createLCR(config) {
2268
2295
  classifyError,
2269
2296
  classifyErrorKind,
2270
2297
  comparePrices,
2298
+ createEnvSink,
2271
2299
  createFalMediaAdapter,
2272
2300
  createHttpSink,
2273
2301
  createKunavoMediaAdapter,
package/dist/index.d.cts CHANGED
@@ -472,6 +472,62 @@ interface HttpSinkOptions {
472
472
  */
473
473
  declare function createHttpSink(options: HttpSinkOptions): (record: CallRecord) => void;
474
474
 
475
+ /**
476
+ * Build an `onCall` sink from env vars, or return undefined when
477
+ * `LCR_INGEST_URL` is not set.
478
+ *
479
+ * Env vars read:
480
+ * - `LCR_INGEST_URL` — dashboard origin (required for the sink to activate)
481
+ * - `LCR_PROJECT` — project tag; falls back to `SITE_KEY` (freeart compat)
482
+ * - `LCR_INGEST_KEY` — optional Bearer token
483
+ */
484
+ declare function createEnvSink(dispatch: (task: () => void | Promise<void>) => void): ((record: CallRecord) => void) | undefined;
485
+
486
+ /**
487
+ * Default provider configs — the URLs + env var names that every portfolio app
488
+ * copy-pastes. Import these instead of redeclaring them per project.
489
+ *
490
+ * Adding a provider here (or changing a URL) propagates to every consumer on
491
+ * the next `npm update ai-lcr` — no per-app code change.
492
+ */
493
+ interface ProviderConfig {
494
+ /** Base URL for the OpenAI-compatible endpoint (no trailing `/v1` unless required). */
495
+ baseURL: string;
496
+ /** Name of the env var that holds the API key. */
497
+ apiKeyEnv: string;
498
+ }
499
+ declare const DEFAULT_PROVIDERS: {
500
+ readonly openrouter: {
501
+ readonly baseURL: "https://openrouter.ai/api/v1";
502
+ readonly apiKeyEnv: "OPENROUTER_API_KEY";
503
+ };
504
+ readonly deepinfra: {
505
+ readonly baseURL: "https://api.deepinfra.com/v1/openai";
506
+ readonly apiKeyEnv: "DEEPINFRA_API_KEY";
507
+ };
508
+ readonly tokenmart: {
509
+ readonly baseURL: "https://model.service-inference.ai/v1";
510
+ readonly apiKeyEnv: "INFERENCE_API_KEY";
511
+ };
512
+ readonly deepseek: {
513
+ readonly baseURL: "https://api.deepseek.com";
514
+ readonly apiKeyEnv: "DEEPSEEK_API_KEY";
515
+ };
516
+ readonly kunavo: {
517
+ readonly baseURL: "https://api.kunavo.com/v1";
518
+ readonly apiKeyEnv: "KUNAVO_API_KEY";
519
+ };
520
+ readonly runware: {
521
+ readonly baseURL: "https://api.runware.ai/v1";
522
+ readonly apiKeyEnv: "RUNWARE_API_KEY";
523
+ };
524
+ readonly fal: {
525
+ readonly baseURL: "https://queue.fal.run";
526
+ readonly apiKeyEnv: "FAL_KEY";
527
+ };
528
+ };
529
+ type DefaultProviderId = keyof typeof DEFAULT_PROVIDERS;
530
+
475
531
  declare const MODEL_PRICES: Record<string, ProviderCost>;
476
532
 
477
533
  /**
@@ -997,13 +1053,28 @@ declare function createFalMediaAdapter(config: FalMediaConfig): MediaAdapter;
997
1053
  * a bundled price table for zero-config cheapest-first ordering.
998
1054
  */
999
1055
 
1056
+ /**
1057
+ * Any object with the `doGenerate` + `doStream` shape — accepts LanguageModelV3
1058
+ * from any version of `@ai-sdk/provider` so consumers don't need a cast when
1059
+ * their transitive `@ai-sdk/provider` version differs from ai-lcr's.
1060
+ */
1061
+ type AnyLanguageModel = {
1062
+ doGenerate: (...args: any[]) => any;
1063
+ doStream: (...args: any[]) => any;
1064
+ provider: string;
1065
+ modelId: string;
1066
+ };
1000
1067
  /**
1001
1068
  * A provider for a model: either a bare AI SDK model (e.g.
1002
1069
  * `createOpenAICompatible(...)("id")`), or that model wrapped with price/label
1003
1070
  * metadata to unlock cost accounting and cheapest-first auto-sorting.
1071
+ *
1072
+ * `model` accepts any AI SDK language model — both LanguageModelV3 and
1073
+ * compatible shapes from other `@ai-sdk/provider` versions — so consumers
1074
+ * never need an `as` cast at the boundary.
1004
1075
  */
1005
- type ProviderEntry = LanguageModelV3 | {
1006
- model: LanguageModelV3;
1076
+ type ProviderEntry = AnyLanguageModel | {
1077
+ model: AnyLanguageModel;
1007
1078
  /** USD per 1M tokens. Enables `onCost` and `autoSort`. */
1008
1079
  cost?: ProviderCost;
1009
1080
  /** Label used in cost events / logs. Defaults to the model's provider id. */
@@ -1128,4 +1199,4 @@ type LCRRouter = (modelName: string) => LanguageModelV3;
1128
1199
  */
1129
1200
  declare function createLCR(config: LCRConfig): LCRRouter;
1130
1201
 
1131
- export { type BillableContext, type CacheOptions, type CacheStore, type CachedCall, type CachedMeta, type CallRecord, type CooldownOptions, type CostEvent, DEFAULT_REFERENCE, type ErrorKind, type FormatOptions, type HttpSinkOptions, type LCRConfig, type LCRRouter, MEDIA_PRICING, MODEL_PRICES, type MediaAdapter, type MediaCostEvent, type MediaGenerateRequest, type MediaGenerateResult, type MediaJobHandle, type MediaJobStatus, type MediaLCR, type MediaLCRConfig, type MediaModality, type MediaModelDef, type MediaOutput, type MediaPollResult, type MediaPricing, type MediaRegistry, type MediaRoute, type MediaRunResult, type MediaStatusRequest, type MediaStatusResult, type MediaSubmitOptions, type MediaSubmitRequest, type MediaSubmitResult, type MediaUnit, type MediaUsage, type MemoryCacheOptions, OFFICIAL_PRICES, type PriceComparisonRow, type PromptCacheOptions, type ProviderCost, type ProviderEntry, type RankedRoute, type ReferenceSpec, type RouteAttempt, billableUnits, cheapestRoute, classifyError, classifyErrorKind, comparePrices, createFalMediaAdapter, createHttpSink, createKunavoMediaAdapter, createLCR, createMediaLCR, createMemoryCacheStore, createRunwareMediaAdapter, durationFromInput, formatCallRecord, getModelPrice, isAbortError, isNetworkError, isRetryableError, normalizedCents, priceCents, rankRoutes, referenceMegapixels, shouldFailover };
1202
+ export { type AnyLanguageModel, type BillableContext, type CacheOptions, type CacheStore, type CachedCall, type CachedMeta, type CallRecord, type CooldownOptions, type CostEvent, DEFAULT_PROVIDERS, DEFAULT_REFERENCE, type DefaultProviderId, type ErrorKind, type FormatOptions, type HttpSinkOptions, type LCRConfig, type LCRRouter, MEDIA_PRICING, MODEL_PRICES, type MediaAdapter, type MediaCostEvent, type MediaGenerateRequest, type MediaGenerateResult, type MediaJobHandle, type MediaJobStatus, type MediaLCR, type MediaLCRConfig, type MediaModality, type MediaModelDef, type MediaOutput, type MediaPollResult, type MediaPricing, type MediaRegistry, type MediaRoute, type MediaRunResult, type MediaStatusRequest, type MediaStatusResult, type MediaSubmitOptions, type MediaSubmitRequest, type MediaSubmitResult, type MediaUnit, type MediaUsage, type MemoryCacheOptions, OFFICIAL_PRICES, type PriceComparisonRow, type PromptCacheOptions, type ProviderConfig, type ProviderCost, type ProviderEntry, type RankedRoute, type ReferenceSpec, type RouteAttempt, billableUnits, cheapestRoute, classifyError, classifyErrorKind, comparePrices, createEnvSink, createFalMediaAdapter, createHttpSink, createKunavoMediaAdapter, createLCR, createMediaLCR, createMemoryCacheStore, createRunwareMediaAdapter, durationFromInput, formatCallRecord, getModelPrice, isAbortError, isNetworkError, isRetryableError, normalizedCents, priceCents, rankRoutes, referenceMegapixels, shouldFailover };
package/dist/index.d.ts CHANGED
@@ -472,6 +472,62 @@ interface HttpSinkOptions {
472
472
  */
473
473
  declare function createHttpSink(options: HttpSinkOptions): (record: CallRecord) => void;
474
474
 
475
+ /**
476
+ * Build an `onCall` sink from env vars, or return undefined when
477
+ * `LCR_INGEST_URL` is not set.
478
+ *
479
+ * Env vars read:
480
+ * - `LCR_INGEST_URL` — dashboard origin (required for the sink to activate)
481
+ * - `LCR_PROJECT` — project tag; falls back to `SITE_KEY` (freeart compat)
482
+ * - `LCR_INGEST_KEY` — optional Bearer token
483
+ */
484
+ declare function createEnvSink(dispatch: (task: () => void | Promise<void>) => void): ((record: CallRecord) => void) | undefined;
485
+
486
+ /**
487
+ * Default provider configs — the URLs + env var names that every portfolio app
488
+ * copy-pastes. Import these instead of redeclaring them per project.
489
+ *
490
+ * Adding a provider here (or changing a URL) propagates to every consumer on
491
+ * the next `npm update ai-lcr` — no per-app code change.
492
+ */
493
+ interface ProviderConfig {
494
+ /** Base URL for the OpenAI-compatible endpoint (no trailing `/v1` unless required). */
495
+ baseURL: string;
496
+ /** Name of the env var that holds the API key. */
497
+ apiKeyEnv: string;
498
+ }
499
+ declare const DEFAULT_PROVIDERS: {
500
+ readonly openrouter: {
501
+ readonly baseURL: "https://openrouter.ai/api/v1";
502
+ readonly apiKeyEnv: "OPENROUTER_API_KEY";
503
+ };
504
+ readonly deepinfra: {
505
+ readonly baseURL: "https://api.deepinfra.com/v1/openai";
506
+ readonly apiKeyEnv: "DEEPINFRA_API_KEY";
507
+ };
508
+ readonly tokenmart: {
509
+ readonly baseURL: "https://model.service-inference.ai/v1";
510
+ readonly apiKeyEnv: "INFERENCE_API_KEY";
511
+ };
512
+ readonly deepseek: {
513
+ readonly baseURL: "https://api.deepseek.com";
514
+ readonly apiKeyEnv: "DEEPSEEK_API_KEY";
515
+ };
516
+ readonly kunavo: {
517
+ readonly baseURL: "https://api.kunavo.com/v1";
518
+ readonly apiKeyEnv: "KUNAVO_API_KEY";
519
+ };
520
+ readonly runware: {
521
+ readonly baseURL: "https://api.runware.ai/v1";
522
+ readonly apiKeyEnv: "RUNWARE_API_KEY";
523
+ };
524
+ readonly fal: {
525
+ readonly baseURL: "https://queue.fal.run";
526
+ readonly apiKeyEnv: "FAL_KEY";
527
+ };
528
+ };
529
+ type DefaultProviderId = keyof typeof DEFAULT_PROVIDERS;
530
+
475
531
  declare const MODEL_PRICES: Record<string, ProviderCost>;
476
532
 
477
533
  /**
@@ -997,13 +1053,28 @@ declare function createFalMediaAdapter(config: FalMediaConfig): MediaAdapter;
997
1053
  * a bundled price table for zero-config cheapest-first ordering.
998
1054
  */
999
1055
 
1056
+ /**
1057
+ * Any object with the `doGenerate` + `doStream` shape — accepts LanguageModelV3
1058
+ * from any version of `@ai-sdk/provider` so consumers don't need a cast when
1059
+ * their transitive `@ai-sdk/provider` version differs from ai-lcr's.
1060
+ */
1061
+ type AnyLanguageModel = {
1062
+ doGenerate: (...args: any[]) => any;
1063
+ doStream: (...args: any[]) => any;
1064
+ provider: string;
1065
+ modelId: string;
1066
+ };
1000
1067
  /**
1001
1068
  * A provider for a model: either a bare AI SDK model (e.g.
1002
1069
  * `createOpenAICompatible(...)("id")`), or that model wrapped with price/label
1003
1070
  * metadata to unlock cost accounting and cheapest-first auto-sorting.
1071
+ *
1072
+ * `model` accepts any AI SDK language model — both LanguageModelV3 and
1073
+ * compatible shapes from other `@ai-sdk/provider` versions — so consumers
1074
+ * never need an `as` cast at the boundary.
1004
1075
  */
1005
- type ProviderEntry = LanguageModelV3 | {
1006
- model: LanguageModelV3;
1076
+ type ProviderEntry = AnyLanguageModel | {
1077
+ model: AnyLanguageModel;
1007
1078
  /** USD per 1M tokens. Enables `onCost` and `autoSort`. */
1008
1079
  cost?: ProviderCost;
1009
1080
  /** Label used in cost events / logs. Defaults to the model's provider id. */
@@ -1128,4 +1199,4 @@ type LCRRouter = (modelName: string) => LanguageModelV3;
1128
1199
  */
1129
1200
  declare function createLCR(config: LCRConfig): LCRRouter;
1130
1201
 
1131
- export { type BillableContext, type CacheOptions, type CacheStore, type CachedCall, type CachedMeta, type CallRecord, type CooldownOptions, type CostEvent, DEFAULT_REFERENCE, type ErrorKind, type FormatOptions, type HttpSinkOptions, type LCRConfig, type LCRRouter, MEDIA_PRICING, MODEL_PRICES, type MediaAdapter, type MediaCostEvent, type MediaGenerateRequest, type MediaGenerateResult, type MediaJobHandle, type MediaJobStatus, type MediaLCR, type MediaLCRConfig, type MediaModality, type MediaModelDef, type MediaOutput, type MediaPollResult, type MediaPricing, type MediaRegistry, type MediaRoute, type MediaRunResult, type MediaStatusRequest, type MediaStatusResult, type MediaSubmitOptions, type MediaSubmitRequest, type MediaSubmitResult, type MediaUnit, type MediaUsage, type MemoryCacheOptions, OFFICIAL_PRICES, type PriceComparisonRow, type PromptCacheOptions, type ProviderCost, type ProviderEntry, type RankedRoute, type ReferenceSpec, type RouteAttempt, billableUnits, cheapestRoute, classifyError, classifyErrorKind, comparePrices, createFalMediaAdapter, createHttpSink, createKunavoMediaAdapter, createLCR, createMediaLCR, createMemoryCacheStore, createRunwareMediaAdapter, durationFromInput, formatCallRecord, getModelPrice, isAbortError, isNetworkError, isRetryableError, normalizedCents, priceCents, rankRoutes, referenceMegapixels, shouldFailover };
1202
+ export { type AnyLanguageModel, type BillableContext, type CacheOptions, type CacheStore, type CachedCall, type CachedMeta, type CallRecord, type CooldownOptions, type CostEvent, DEFAULT_PROVIDERS, DEFAULT_REFERENCE, type DefaultProviderId, type ErrorKind, type FormatOptions, type HttpSinkOptions, type LCRConfig, type LCRRouter, MEDIA_PRICING, MODEL_PRICES, type MediaAdapter, type MediaCostEvent, type MediaGenerateRequest, type MediaGenerateResult, type MediaJobHandle, type MediaJobStatus, type MediaLCR, type MediaLCRConfig, type MediaModality, type MediaModelDef, type MediaOutput, type MediaPollResult, type MediaPricing, type MediaRegistry, type MediaRoute, type MediaRunResult, type MediaStatusRequest, type MediaStatusResult, type MediaSubmitOptions, type MediaSubmitRequest, type MediaSubmitResult, type MediaUnit, type MediaUsage, type MemoryCacheOptions, OFFICIAL_PRICES, type PriceComparisonRow, type PromptCacheOptions, type ProviderConfig, type ProviderCost, type ProviderEntry, type RankedRoute, type ReferenceSpec, type RouteAttempt, billableUnits, cheapestRoute, classifyError, classifyErrorKind, comparePrices, createEnvSink, createFalMediaAdapter, createHttpSink, createKunavoMediaAdapter, createLCR, createMediaLCR, createMemoryCacheStore, createRunwareMediaAdapter, durationFromInput, formatCallRecord, getModelPrice, isAbortError, isNetworkError, isRetryableError, normalizedCents, priceCents, rankRoutes, referenceMegapixels, shouldFailover };
package/dist/index.js CHANGED
@@ -853,6 +853,30 @@ function createHttpSink(options) {
853
853
  };
854
854
  }
855
855
 
856
+ // src/env-sink.ts
857
+ function createEnvSink(dispatch) {
858
+ const base = process.env.LCR_INGEST_URL?.replace(/\/+$/, "");
859
+ if (!base) return void 0;
860
+ return createHttpSink({
861
+ url: `${base}/api/ingest`,
862
+ headers: process.env.LCR_INGEST_KEY ? { authorization: `Bearer ${process.env.LCR_INGEST_KEY}` } : void 0,
863
+ project: process.env.LCR_PROJECT ?? process.env.SITE_KEY,
864
+ dispatch,
865
+ onError: (err) => console.error("[lcr] ingest POST failed:", err)
866
+ });
867
+ }
868
+
869
+ // src/providers.ts
870
+ var DEFAULT_PROVIDERS = {
871
+ openrouter: { baseURL: "https://openrouter.ai/api/v1", apiKeyEnv: "OPENROUTER_API_KEY" },
872
+ deepinfra: { baseURL: "https://api.deepinfra.com/v1/openai", apiKeyEnv: "DEEPINFRA_API_KEY" },
873
+ tokenmart: { baseURL: "https://model.service-inference.ai/v1", apiKeyEnv: "INFERENCE_API_KEY" },
874
+ deepseek: { baseURL: "https://api.deepseek.com", apiKeyEnv: "DEEPSEEK_API_KEY" },
875
+ kunavo: { baseURL: "https://api.kunavo.com/v1", apiKeyEnv: "KUNAVO_API_KEY" },
876
+ runware: { baseURL: "https://api.runware.ai/v1", apiKeyEnv: "RUNWARE_API_KEY" },
877
+ fal: { baseURL: "https://queue.fal.run", apiKeyEnv: "FAL_KEY" }
878
+ };
879
+
856
880
  // src/text-prices.ts
857
881
  var MODEL_PRICES = {
858
882
  "chatgpt-4o-latest": { input: 5, output: 15 },
@@ -2206,6 +2230,7 @@ function createLCR(config) {
2206
2230
  };
2207
2231
  }
2208
2232
  export {
2233
+ DEFAULT_PROVIDERS,
2209
2234
  DEFAULT_REFERENCE,
2210
2235
  MEDIA_PRICING,
2211
2236
  MODEL_PRICES,
@@ -2215,6 +2240,7 @@ export {
2215
2240
  classifyError,
2216
2241
  classifyErrorKind,
2217
2242
  comparePrices,
2243
+ createEnvSink,
2218
2244
  createFalMediaAdapter,
2219
2245
  createHttpSink,
2220
2246
  createKunavoMediaAdapter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-lcr",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Least Cost Routing for LLMs — route every model call to the cheapest available provider, fall back automatically, and track real cost. Built for the Vercel AI SDK.",
5
5
  "keywords": [
6
6
  "ai",