@lunora/ai 1.0.0-alpha.126 → 1.0.0-alpha.128
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/create.d.mts +96 -0
- package/dist/create.d.ts +96 -0
- package/dist/create.mjs +1 -0
- package/dist/index.d.mts +2 -94
- package/dist/index.d.ts +2 -94
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/VECTORIZE_CAPABILITIES-l9ceRO5j.mjs +1 -0
- package/dist/packem_shared/createAi-Bufv-O1b.mjs +1 -0
- package/dist/packem_shared/defineRag-CJudmmcj.mjs +8 -0
- package/dist/packem_shared/ragSyncTriggers-D933fCwt.mjs +1 -0
- package/dist/packem_shared/sqliteVectorStore-Dyv1x3eC.mjs +1 -0
- package/dist/packem_shared/{types.d-D-WK1t2B.d.mts → types.d-DL0j0z-W.d.mts} +13 -4
- package/dist/packem_shared/{types.d-D-WK1t2B.d.ts → types.d-DL0j0z-W.d.ts} +13 -4
- package/dist/rag/index.d.mts +22 -5
- package/dist/rag/index.d.ts +22 -5
- package/dist/rag/index.mjs +1 -1
- package/package.json +7 -1
- package/dist/packem_shared/VECTORIZE_CAPABILITIES-CUQDoxis.mjs +0 -1
- package/dist/packem_shared/createAi-DDJthsZF.mjs +0 -1
- package/dist/packem_shared/defineRag-CMTzKfS7.mjs +0 -8
- package/dist/packem_shared/ragSyncTriggers-hgMcA4f2.mjs +0 -1
- package/dist/packem_shared/sqliteVectorStore-SjOFKoHo.mjs +0 -1
- package/dist/packem_shared/stable-key-B_BlboiY.mjs +0 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { a as LunoraAiOptions, L as LunoraAi } from "./packem_shared/types.d-DL0j0z-W.mjs";
|
|
2
|
+
export type { A as AiBindingLike, b as AiWebSearchItem, c as AiWebSearchProvider } from "./packem_shared/types.d-DL0j0z-W.mjs";
|
|
3
|
+
import 'ai';
|
|
4
|
+
import 'workers-ai-provider';
|
|
5
|
+
/**
|
|
6
|
+
* Create the `ctx.ai` helper over a Workers `AI` binding.
|
|
7
|
+
*
|
|
8
|
+
* Workers AI is the zero-config default, but `@lunora/ai` is provider-agnostic:
|
|
9
|
+
* every helper takes either a model id string (resolved against the Workers AI
|
|
10
|
+
* provider) or any AI SDK {@link LanguageModel}/{@link EmbeddingModel} object
|
|
11
|
+
* (`@ai-sdk/openai`, `@ai-sdk/anthropic`, OpenRouter, …), so apps are never
|
|
12
|
+
* locked to Workers AI. Pair `embed` with `@lunora/bindings/vectors` for RAG.
|
|
13
|
+
*
|
|
14
|
+
* Without a binding, a string id resolves only as a `"<provider>/<model>"` slug
|
|
15
|
+
* through {@link AI_PROXY_URL_ENV}; everything else that needs the binding
|
|
16
|
+
* throws a directed error when called, never at construction — so the
|
|
17
|
+
* generated `ctx.ai` is always this facade.
|
|
18
|
+
*
|
|
19
|
+
* Combine with the re-exported `generateText`/`streamText`/`generateObject`/
|
|
20
|
+
* `embed`/`tool` from this package:
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { streamText } from "@lunora/ai";
|
|
24
|
+
*
|
|
25
|
+
* const result = streamText({
|
|
26
|
+
* model: ctx.ai.model("@cf/meta/llama-3.3-70b-instruct-fp8-fast"),
|
|
27
|
+
* messages,
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
* @experimental
|
|
31
|
+
*/
|
|
32
|
+
declare const createAi: (options: LunoraAiOptions) => LunoraAi;
|
|
33
|
+
/**
|
|
34
|
+
* Model cost estimation without an AI Gateway.
|
|
35
|
+
*
|
|
36
|
+
* Per-request dollar cost previously reached a span only when a Cloudflare AI
|
|
37
|
+
* Gateway put it in `providerMetadata`. Run the same model through
|
|
38
|
+
* `@ai-sdk/openai` directly, or on any non-Cloudflare host, and spend
|
|
39
|
+
* visibility silently disappeared — inside a telemetry stack that is otherwise
|
|
40
|
+
* host-neutral by design.
|
|
41
|
+
*
|
|
42
|
+
* This derives cost from token usage and a price table instead, so the number
|
|
43
|
+
* is there either way.
|
|
44
|
+
*
|
|
45
|
+
* **An estimate is never presented as a measurement.** A provider-reported cost
|
|
46
|
+
* always wins, and a span carrying an estimate is tagged
|
|
47
|
+
* `lunora.usage.cost.source: "estimated"` so a dashboard can tell the two
|
|
48
|
+
* apart. Getting that wrong turns a rounding error into a billing dispute.
|
|
49
|
+
*
|
|
50
|
+
* **Prices go stale.** The shipped table is indicative, not authoritative — it
|
|
51
|
+
* is a hand-maintained snapshot, and providers change prices without warning.
|
|
52
|
+
* Pass your own `prices` for anything you are actually invoicing against.
|
|
53
|
+
* @experimental
|
|
54
|
+
*/
|
|
55
|
+
/** What one model costs, in USD per **one million** tokens. */
|
|
56
|
+
interface ModelPrice {
|
|
57
|
+
/** Price per million input (prompt) tokens. */
|
|
58
|
+
input: number;
|
|
59
|
+
/** Price per million output (completion) tokens. Omit for an embedding model. */
|
|
60
|
+
output?: number;
|
|
61
|
+
}
|
|
62
|
+
/** Token counts to price. Either may be absent. */
|
|
63
|
+
interface ModelUsage {
|
|
64
|
+
inputTokens?: number;
|
|
65
|
+
outputTokens?: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* An indicative price table, keyed by model id.
|
|
69
|
+
*
|
|
70
|
+
* Deliberately small: a table that tries to cover every model is a table that
|
|
71
|
+
* is wrong about most of them. It holds the models Lunora's own defaults and
|
|
72
|
+
* documented examples reference — text generation and embeddings, on Workers AI
|
|
73
|
+
* and OpenAI — and everything else returns `undefined` rather than a guess.
|
|
74
|
+
*
|
|
75
|
+
* Generation models carry an `output` price; embedding models do not (they have
|
|
76
|
+
* no completion). A generation model missing from here is why a chat span would
|
|
77
|
+
* carry no cost at all off an AI Gateway, so the ids the docs teach are the ones
|
|
78
|
+
* that have to be in the table.
|
|
79
|
+
*/
|
|
80
|
+
declare const DEFAULT_MODEL_PRICES: Readonly<Record<string, ModelPrice>>;
|
|
81
|
+
/**
|
|
82
|
+
* Look up a model's price, or `undefined` when the table does not cover it.
|
|
83
|
+
* @experimental
|
|
84
|
+
*/
|
|
85
|
+
declare const lookupModelPrice: (modelId: string, prices?: Readonly<Record<string, ModelPrice>>) => ModelPrice | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Estimate a call's cost in USD from its token usage, or `undefined` when the
|
|
88
|
+
* model is not priced or no usable token count was supplied.
|
|
89
|
+
*
|
|
90
|
+
* Returns `undefined` rather than `0` for an unpriced model: zero is a
|
|
91
|
+
* defensible cost that would quietly sum into a total, while an absent value
|
|
92
|
+
* shows up as absent.
|
|
93
|
+
* @experimental
|
|
94
|
+
*/
|
|
95
|
+
declare const estimateModelCost: (modelId: string | undefined, usage: ModelUsage, prices?: Readonly<Record<string, ModelPrice>>) => number | undefined;
|
|
96
|
+
export { DEFAULT_MODEL_PRICES as D, type LunoraAi, ModelPrice as M, ModelUsage as a, createAi, estimateModelCost, lookupModelPrice as l };
|
package/dist/create.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { a as LunoraAiOptions, L as LunoraAi } from "./packem_shared/types.d-DL0j0z-W.js";
|
|
2
|
+
export type { A as AiBindingLike, b as AiWebSearchItem, c as AiWebSearchProvider } from "./packem_shared/types.d-DL0j0z-W.js";
|
|
3
|
+
import 'ai';
|
|
4
|
+
import 'workers-ai-provider';
|
|
5
|
+
/**
|
|
6
|
+
* Create the `ctx.ai` helper over a Workers `AI` binding.
|
|
7
|
+
*
|
|
8
|
+
* Workers AI is the zero-config default, but `@lunora/ai` is provider-agnostic:
|
|
9
|
+
* every helper takes either a model id string (resolved against the Workers AI
|
|
10
|
+
* provider) or any AI SDK {@link LanguageModel}/{@link EmbeddingModel} object
|
|
11
|
+
* (`@ai-sdk/openai`, `@ai-sdk/anthropic`, OpenRouter, …), so apps are never
|
|
12
|
+
* locked to Workers AI. Pair `embed` with `@lunora/bindings/vectors` for RAG.
|
|
13
|
+
*
|
|
14
|
+
* Without a binding, a string id resolves only as a `"<provider>/<model>"` slug
|
|
15
|
+
* through {@link AI_PROXY_URL_ENV}; everything else that needs the binding
|
|
16
|
+
* throws a directed error when called, never at construction — so the
|
|
17
|
+
* generated `ctx.ai` is always this facade.
|
|
18
|
+
*
|
|
19
|
+
* Combine with the re-exported `generateText`/`streamText`/`generateObject`/
|
|
20
|
+
* `embed`/`tool` from this package:
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { streamText } from "@lunora/ai";
|
|
24
|
+
*
|
|
25
|
+
* const result = streamText({
|
|
26
|
+
* model: ctx.ai.model("@cf/meta/llama-3.3-70b-instruct-fp8-fast"),
|
|
27
|
+
* messages,
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
* @experimental
|
|
31
|
+
*/
|
|
32
|
+
declare const createAi: (options: LunoraAiOptions) => LunoraAi;
|
|
33
|
+
/**
|
|
34
|
+
* Model cost estimation without an AI Gateway.
|
|
35
|
+
*
|
|
36
|
+
* Per-request dollar cost previously reached a span only when a Cloudflare AI
|
|
37
|
+
* Gateway put it in `providerMetadata`. Run the same model through
|
|
38
|
+
* `@ai-sdk/openai` directly, or on any non-Cloudflare host, and spend
|
|
39
|
+
* visibility silently disappeared — inside a telemetry stack that is otherwise
|
|
40
|
+
* host-neutral by design.
|
|
41
|
+
*
|
|
42
|
+
* This derives cost from token usage and a price table instead, so the number
|
|
43
|
+
* is there either way.
|
|
44
|
+
*
|
|
45
|
+
* **An estimate is never presented as a measurement.** A provider-reported cost
|
|
46
|
+
* always wins, and a span carrying an estimate is tagged
|
|
47
|
+
* `lunora.usage.cost.source: "estimated"` so a dashboard can tell the two
|
|
48
|
+
* apart. Getting that wrong turns a rounding error into a billing dispute.
|
|
49
|
+
*
|
|
50
|
+
* **Prices go stale.** The shipped table is indicative, not authoritative — it
|
|
51
|
+
* is a hand-maintained snapshot, and providers change prices without warning.
|
|
52
|
+
* Pass your own `prices` for anything you are actually invoicing against.
|
|
53
|
+
* @experimental
|
|
54
|
+
*/
|
|
55
|
+
/** What one model costs, in USD per **one million** tokens. */
|
|
56
|
+
interface ModelPrice {
|
|
57
|
+
/** Price per million input (prompt) tokens. */
|
|
58
|
+
input: number;
|
|
59
|
+
/** Price per million output (completion) tokens. Omit for an embedding model. */
|
|
60
|
+
output?: number;
|
|
61
|
+
}
|
|
62
|
+
/** Token counts to price. Either may be absent. */
|
|
63
|
+
interface ModelUsage {
|
|
64
|
+
inputTokens?: number;
|
|
65
|
+
outputTokens?: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* An indicative price table, keyed by model id.
|
|
69
|
+
*
|
|
70
|
+
* Deliberately small: a table that tries to cover every model is a table that
|
|
71
|
+
* is wrong about most of them. It holds the models Lunora's own defaults and
|
|
72
|
+
* documented examples reference — text generation and embeddings, on Workers AI
|
|
73
|
+
* and OpenAI — and everything else returns `undefined` rather than a guess.
|
|
74
|
+
*
|
|
75
|
+
* Generation models carry an `output` price; embedding models do not (they have
|
|
76
|
+
* no completion). A generation model missing from here is why a chat span would
|
|
77
|
+
* carry no cost at all off an AI Gateway, so the ids the docs teach are the ones
|
|
78
|
+
* that have to be in the table.
|
|
79
|
+
*/
|
|
80
|
+
declare const DEFAULT_MODEL_PRICES: Readonly<Record<string, ModelPrice>>;
|
|
81
|
+
/**
|
|
82
|
+
* Look up a model's price, or `undefined` when the table does not cover it.
|
|
83
|
+
* @experimental
|
|
84
|
+
*/
|
|
85
|
+
declare const lookupModelPrice: (modelId: string, prices?: Readonly<Record<string, ModelPrice>>) => ModelPrice | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Estimate a call's cost in USD from its token usage, or `undefined` when the
|
|
88
|
+
* model is not priced or no usable token count was supplied.
|
|
89
|
+
*
|
|
90
|
+
* Returns `undefined` rather than `0` for an unpriced model: zero is a
|
|
91
|
+
* defensible cost that would quietly sum into a total, while an absent value
|
|
92
|
+
* shows up as absent.
|
|
93
|
+
* @experimental
|
|
94
|
+
*/
|
|
95
|
+
declare const estimateModelCost: (modelId: string | undefined, usage: ModelUsage, prices?: Readonly<Record<string, ModelPrice>>) => number | undefined;
|
|
96
|
+
export { DEFAULT_MODEL_PRICES as D, type LunoraAi, ModelPrice as M, ModelUsage as a, createAi, estimateModelCost, lookupModelPrice as l };
|
package/dist/create.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{default as o}from"./packem_shared/createAi-Bufv-O1b.mjs";import{estimateModelCost as a}from"./packem_shared/DEFAULT_MODEL_PRICES-CIJLs1Ah.mjs";export{o as createAi,a as estimateModelCost};
|
package/dist/index.d.mts
CHANGED
|
@@ -1,96 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
1
|
+
export { D as DEFAULT_MODEL_PRICES, type M as ModelPrice, type a as ModelUsage, createAi, estimateModelCost, l as lookupModelPrice } from "./create.mjs";
|
|
2
|
+
export { d as AI_DEFAULT_EMBEDDING_MODEL_ENV, e as AI_DEFAULT_MODEL_ENV, f as AI_GATEWAY_ACCOUNT_ID_ENV, g as AI_GATEWAY_ID_ENV, h as AI_GATEWAY_METADATA_MAX_KEYS, i as AI_GATEWAY_TAGS_ENV, j as AI_GATEWAY_TOKEN_ENV, k as AI_PROXY_TOKEN_ENV, l as AI_PROXY_URL_ENV, type A as AiBindingLike, type m as AiGatewayMetadata, type n as AiGatewayOptions, type o as AiMetrics, type p as AiModelOptions, type q as AiRunOptions, type r as AiSpan, type s as AiTelemetry, type t as AiTracer, type b as AiWebSearchItem, type u as AiWebSearchOptions, type c as AiWebSearchProvider, type v as AiWebSearchResult, type E as EmbeddingModelInput, type L as LunoraAi, type a as LunoraAiOptions, type M as ModelInput, type R as ResolvedAiGateway, type W as WorkersAiProviderLike, w as buildAiGatewayMetadataFields, x as readAiGatewayEnvTags, y as resolveAiGateway } from "./packem_shared/types.d-DL0j0z-W.mjs";
|
|
3
3
|
export { type EmbeddingModel, type LanguageModel, embed, embedMany, generateObject, generateText, hasToolCall, jsonSchema, streamObject, streamText, tool } from 'ai';
|
|
4
4
|
export { createWorkersAI } from 'workers-ai-provider';
|
|
5
|
-
/**
|
|
6
|
-
* Create the `ctx.ai` helper over a Workers `AI` binding.
|
|
7
|
-
*
|
|
8
|
-
* Workers AI is the zero-config default, but `@lunora/ai` is provider-agnostic:
|
|
9
|
-
* every helper takes either a model id string (resolved against the Workers AI
|
|
10
|
-
* provider) or any AI SDK {@link LanguageModel}/{@link EmbeddingModel} object
|
|
11
|
-
* (`@ai-sdk/openai`, `@ai-sdk/anthropic`, OpenRouter, …), so apps are never
|
|
12
|
-
* locked to Workers AI. Pair `embed` with `@lunora/bindings/vectors` for RAG.
|
|
13
|
-
*
|
|
14
|
-
* Without a binding, a string id resolves only as a `"<provider>/<model>"` slug
|
|
15
|
-
* through {@link AI_PROXY_URL_ENV}; everything else that needs the binding
|
|
16
|
-
* throws a directed error when called, never at construction — so the
|
|
17
|
-
* generated `ctx.ai` is always this facade.
|
|
18
|
-
*
|
|
19
|
-
* Combine with the re-exported `generateText`/`streamText`/`generateObject`/
|
|
20
|
-
* `embed`/`tool` from this package:
|
|
21
|
-
*
|
|
22
|
-
* ```ts
|
|
23
|
-
* import { streamText } from "@lunora/ai";
|
|
24
|
-
*
|
|
25
|
-
* const result = streamText({
|
|
26
|
-
* model: ctx.ai.model("@cf/meta/llama-3.3-70b-instruct-fp8-fast"),
|
|
27
|
-
* messages,
|
|
28
|
-
* });
|
|
29
|
-
* ```
|
|
30
|
-
* @experimental
|
|
31
|
-
*/
|
|
32
|
-
declare const createAi: (options: LunoraAiOptions) => LunoraAi;
|
|
33
|
-
/**
|
|
34
|
-
* Model cost estimation without an AI Gateway.
|
|
35
|
-
*
|
|
36
|
-
* Per-request dollar cost previously reached a span only when a Cloudflare AI
|
|
37
|
-
* Gateway put it in `providerMetadata`. Run the same model through
|
|
38
|
-
* `@ai-sdk/openai` directly, or on any non-Cloudflare host, and spend
|
|
39
|
-
* visibility silently disappeared — inside a telemetry stack that is otherwise
|
|
40
|
-
* host-neutral by design.
|
|
41
|
-
*
|
|
42
|
-
* This derives cost from token usage and a price table instead, so the number
|
|
43
|
-
* is there either way.
|
|
44
|
-
*
|
|
45
|
-
* **An estimate is never presented as a measurement.** A provider-reported cost
|
|
46
|
-
* always wins, and a span carrying an estimate is tagged
|
|
47
|
-
* `lunora.usage.cost.source: "estimated"` so a dashboard can tell the two
|
|
48
|
-
* apart. Getting that wrong turns a rounding error into a billing dispute.
|
|
49
|
-
*
|
|
50
|
-
* **Prices go stale.** The shipped table is indicative, not authoritative — it
|
|
51
|
-
* is a hand-maintained snapshot, and providers change prices without warning.
|
|
52
|
-
* Pass your own `prices` for anything you are actually invoicing against.
|
|
53
|
-
* @experimental
|
|
54
|
-
*/
|
|
55
|
-
/** What one model costs, in USD per **one million** tokens. */
|
|
56
|
-
interface ModelPrice {
|
|
57
|
-
/** Price per million input (prompt) tokens. */
|
|
58
|
-
input: number;
|
|
59
|
-
/** Price per million output (completion) tokens. Omit for an embedding model. */
|
|
60
|
-
output?: number;
|
|
61
|
-
}
|
|
62
|
-
/** Token counts to price. Either may be absent. */
|
|
63
|
-
interface ModelUsage {
|
|
64
|
-
inputTokens?: number;
|
|
65
|
-
outputTokens?: number;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* An indicative price table, keyed by model id.
|
|
69
|
-
*
|
|
70
|
-
* Deliberately small: a table that tries to cover every model is a table that
|
|
71
|
-
* is wrong about most of them. It holds the models Lunora's own defaults and
|
|
72
|
-
* documented examples reference — text generation and embeddings, on Workers AI
|
|
73
|
-
* and OpenAI — and everything else returns `undefined` rather than a guess.
|
|
74
|
-
*
|
|
75
|
-
* Generation models carry an `output` price; embedding models do not (they have
|
|
76
|
-
* no completion). A generation model missing from here is why a chat span would
|
|
77
|
-
* carry no cost at all off an AI Gateway, so the ids the docs teach are the ones
|
|
78
|
-
* that have to be in the table.
|
|
79
|
-
*/
|
|
80
|
-
declare const DEFAULT_MODEL_PRICES: Readonly<Record<string, ModelPrice>>;
|
|
81
|
-
/**
|
|
82
|
-
* Look up a model's price, or `undefined` when the table does not cover it.
|
|
83
|
-
* @experimental
|
|
84
|
-
*/
|
|
85
|
-
declare const lookupModelPrice: (modelId: string, prices?: Readonly<Record<string, ModelPrice>>) => ModelPrice | undefined;
|
|
86
|
-
/**
|
|
87
|
-
* Estimate a call's cost in USD from its token usage, or `undefined` when the
|
|
88
|
-
* model is not priced or no usable token count was supplied.
|
|
89
|
-
*
|
|
90
|
-
* Returns `undefined` rather than `0` for an unpriced model: zero is a
|
|
91
|
-
* defensible cost that would quietly sum into a total, while an absent value
|
|
92
|
-
* shows up as absent.
|
|
93
|
-
* @experimental
|
|
94
|
-
*/
|
|
95
|
-
declare const estimateModelCost: (modelId: string | undefined, usage: ModelUsage, prices?: Readonly<Record<string, ModelPrice>>) => number | undefined;
|
|
96
|
-
export { DEFAULT_MODEL_PRICES, type LunoraAi, type LunoraAiOptions, type ModelPrice, type ModelUsage, createAi, estimateModelCost, lookupModelPrice };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,96 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
1
|
+
export { D as DEFAULT_MODEL_PRICES, type M as ModelPrice, type a as ModelUsage, createAi, estimateModelCost, l as lookupModelPrice } from "./create.js";
|
|
2
|
+
export { d as AI_DEFAULT_EMBEDDING_MODEL_ENV, e as AI_DEFAULT_MODEL_ENV, f as AI_GATEWAY_ACCOUNT_ID_ENV, g as AI_GATEWAY_ID_ENV, h as AI_GATEWAY_METADATA_MAX_KEYS, i as AI_GATEWAY_TAGS_ENV, j as AI_GATEWAY_TOKEN_ENV, k as AI_PROXY_TOKEN_ENV, l as AI_PROXY_URL_ENV, type A as AiBindingLike, type m as AiGatewayMetadata, type n as AiGatewayOptions, type o as AiMetrics, type p as AiModelOptions, type q as AiRunOptions, type r as AiSpan, type s as AiTelemetry, type t as AiTracer, type b as AiWebSearchItem, type u as AiWebSearchOptions, type c as AiWebSearchProvider, type v as AiWebSearchResult, type E as EmbeddingModelInput, type L as LunoraAi, type a as LunoraAiOptions, type M as ModelInput, type R as ResolvedAiGateway, type W as WorkersAiProviderLike, w as buildAiGatewayMetadataFields, x as readAiGatewayEnvTags, y as resolveAiGateway } from "./packem_shared/types.d-DL0j0z-W.js";
|
|
3
3
|
export { type EmbeddingModel, type LanguageModel, embed, embedMany, generateObject, generateText, hasToolCall, jsonSchema, streamObject, streamText, tool } from 'ai';
|
|
4
4
|
export { createWorkersAI } from 'workers-ai-provider';
|
|
5
|
-
/**
|
|
6
|
-
* Create the `ctx.ai` helper over a Workers `AI` binding.
|
|
7
|
-
*
|
|
8
|
-
* Workers AI is the zero-config default, but `@lunora/ai` is provider-agnostic:
|
|
9
|
-
* every helper takes either a model id string (resolved against the Workers AI
|
|
10
|
-
* provider) or any AI SDK {@link LanguageModel}/{@link EmbeddingModel} object
|
|
11
|
-
* (`@ai-sdk/openai`, `@ai-sdk/anthropic`, OpenRouter, …), so apps are never
|
|
12
|
-
* locked to Workers AI. Pair `embed` with `@lunora/bindings/vectors` for RAG.
|
|
13
|
-
*
|
|
14
|
-
* Without a binding, a string id resolves only as a `"<provider>/<model>"` slug
|
|
15
|
-
* through {@link AI_PROXY_URL_ENV}; everything else that needs the binding
|
|
16
|
-
* throws a directed error when called, never at construction — so the
|
|
17
|
-
* generated `ctx.ai` is always this facade.
|
|
18
|
-
*
|
|
19
|
-
* Combine with the re-exported `generateText`/`streamText`/`generateObject`/
|
|
20
|
-
* `embed`/`tool` from this package:
|
|
21
|
-
*
|
|
22
|
-
* ```ts
|
|
23
|
-
* import { streamText } from "@lunora/ai";
|
|
24
|
-
*
|
|
25
|
-
* const result = streamText({
|
|
26
|
-
* model: ctx.ai.model("@cf/meta/llama-3.3-70b-instruct-fp8-fast"),
|
|
27
|
-
* messages,
|
|
28
|
-
* });
|
|
29
|
-
* ```
|
|
30
|
-
* @experimental
|
|
31
|
-
*/
|
|
32
|
-
declare const createAi: (options: LunoraAiOptions) => LunoraAi;
|
|
33
|
-
/**
|
|
34
|
-
* Model cost estimation without an AI Gateway.
|
|
35
|
-
*
|
|
36
|
-
* Per-request dollar cost previously reached a span only when a Cloudflare AI
|
|
37
|
-
* Gateway put it in `providerMetadata`. Run the same model through
|
|
38
|
-
* `@ai-sdk/openai` directly, or on any non-Cloudflare host, and spend
|
|
39
|
-
* visibility silently disappeared — inside a telemetry stack that is otherwise
|
|
40
|
-
* host-neutral by design.
|
|
41
|
-
*
|
|
42
|
-
* This derives cost from token usage and a price table instead, so the number
|
|
43
|
-
* is there either way.
|
|
44
|
-
*
|
|
45
|
-
* **An estimate is never presented as a measurement.** A provider-reported cost
|
|
46
|
-
* always wins, and a span carrying an estimate is tagged
|
|
47
|
-
* `lunora.usage.cost.source: "estimated"` so a dashboard can tell the two
|
|
48
|
-
* apart. Getting that wrong turns a rounding error into a billing dispute.
|
|
49
|
-
*
|
|
50
|
-
* **Prices go stale.** The shipped table is indicative, not authoritative — it
|
|
51
|
-
* is a hand-maintained snapshot, and providers change prices without warning.
|
|
52
|
-
* Pass your own `prices` for anything you are actually invoicing against.
|
|
53
|
-
* @experimental
|
|
54
|
-
*/
|
|
55
|
-
/** What one model costs, in USD per **one million** tokens. */
|
|
56
|
-
interface ModelPrice {
|
|
57
|
-
/** Price per million input (prompt) tokens. */
|
|
58
|
-
input: number;
|
|
59
|
-
/** Price per million output (completion) tokens. Omit for an embedding model. */
|
|
60
|
-
output?: number;
|
|
61
|
-
}
|
|
62
|
-
/** Token counts to price. Either may be absent. */
|
|
63
|
-
interface ModelUsage {
|
|
64
|
-
inputTokens?: number;
|
|
65
|
-
outputTokens?: number;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* An indicative price table, keyed by model id.
|
|
69
|
-
*
|
|
70
|
-
* Deliberately small: a table that tries to cover every model is a table that
|
|
71
|
-
* is wrong about most of them. It holds the models Lunora's own defaults and
|
|
72
|
-
* documented examples reference — text generation and embeddings, on Workers AI
|
|
73
|
-
* and OpenAI — and everything else returns `undefined` rather than a guess.
|
|
74
|
-
*
|
|
75
|
-
* Generation models carry an `output` price; embedding models do not (they have
|
|
76
|
-
* no completion). A generation model missing from here is why a chat span would
|
|
77
|
-
* carry no cost at all off an AI Gateway, so the ids the docs teach are the ones
|
|
78
|
-
* that have to be in the table.
|
|
79
|
-
*/
|
|
80
|
-
declare const DEFAULT_MODEL_PRICES: Readonly<Record<string, ModelPrice>>;
|
|
81
|
-
/**
|
|
82
|
-
* Look up a model's price, or `undefined` when the table does not cover it.
|
|
83
|
-
* @experimental
|
|
84
|
-
*/
|
|
85
|
-
declare const lookupModelPrice: (modelId: string, prices?: Readonly<Record<string, ModelPrice>>) => ModelPrice | undefined;
|
|
86
|
-
/**
|
|
87
|
-
* Estimate a call's cost in USD from its token usage, or `undefined` when the
|
|
88
|
-
* model is not priced or no usable token count was supplied.
|
|
89
|
-
*
|
|
90
|
-
* Returns `undefined` rather than `0` for an unpriced model: zero is a
|
|
91
|
-
* defensible cost that would quietly sum into a total, while an absent value
|
|
92
|
-
* shows up as absent.
|
|
93
|
-
* @experimental
|
|
94
|
-
*/
|
|
95
|
-
declare const estimateModelCost: (modelId: string | undefined, usage: ModelUsage, prices?: Readonly<Record<string, ModelPrice>>) => number | undefined;
|
|
96
|
-
export { DEFAULT_MODEL_PRICES, type LunoraAi, type LunoraAiOptions, type ModelPrice, type ModelUsage, createAi, estimateModelCost, lookupModelPrice };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{default as _}from"./packem_shared/createAi-
|
|
1
|
+
import{default as _}from"./packem_shared/createAi-Bufv-O1b.mjs";import{AI_DEFAULT_EMBEDDING_MODEL_ENV as t,AI_DEFAULT_MODEL_ENV as a,AI_GATEWAY_ACCOUNT_ID_ENV as o,AI_GATEWAY_ID_ENV as r,AI_GATEWAY_METADATA_MAX_KEYS as T,AI_GATEWAY_TAGS_ENV as I,AI_GATEWAY_TOKEN_ENV as N,AI_PROXY_TOKEN_ENV as l,AI_PROXY_URL_ENV as m,buildAiGatewayMetadataFields as s,readAiGatewayEnvTags as D,resolveAiGateway as G}from"./packem_shared/AI_DEFAULT_EMBEDDING_MODEL_ENV-IwcHZiKj.mjs";import{DEFAULT_MODEL_PRICES as O,estimateModelCost as d,lookupModelPrice as i}from"./packem_shared/DEFAULT_MODEL_PRICES-CIJLs1Ah.mjs";import{embed as Y,embedMany as x,generateObject as L,generateText as c,hasToolCall as f,jsonSchema as p,streamObject as W,streamText as b,tool as n}from"ai";import{createWorkersAI as U}from"workers-ai-provider";export{t as AI_DEFAULT_EMBEDDING_MODEL_ENV,a as AI_DEFAULT_MODEL_ENV,o as AI_GATEWAY_ACCOUNT_ID_ENV,r as AI_GATEWAY_ID_ENV,T as AI_GATEWAY_METADATA_MAX_KEYS,I as AI_GATEWAY_TAGS_ENV,N as AI_GATEWAY_TOKEN_ENV,l as AI_PROXY_TOKEN_ENV,m as AI_PROXY_URL_ENV,O as DEFAULT_MODEL_PRICES,s as buildAiGatewayMetadataFields,_ as createAi,U as createWorkersAI,Y as embed,x as embedMany,d as estimateModelCost,L as generateObject,c as generateText,f as hasToolCall,p as jsonSchema,i as lookupModelPrice,D as readAiGatewayEnvTags,G as resolveAiGateway,W as streamObject,b as streamText,n as tool};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const y={maxDimensions:1536,maxIdBytes:64,maxMetadataBytes:10240,maxTopK:100,maxTopKWithMetadata:50},p=(t,r)=>({...t.upsertMany?{upsertMany:(e,a)=>t.upsertMany?.(r,e,a)}:{},capabilities:t.deferredDelete?{...y,deferredDelete:!0}:y,deleteByIds:(e,a,s,d)=>t.deleteByIds(r,e,a,s,d),getByIds:(e,a)=>t.getByIds(r,e,a),query:e=>t.query(r,e),upsert:(e,a)=>t.upsert(r,e,a)});export{y as VECTORIZE_CAPABILITIES,p as vectorizeStore};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./DEFAULT_MODEL_PRICES-CIJLs1Ah.mjs";import{LunoraError as g}from"@lunora/errors";import{buildAiGatewayMetadataFields as W,readEnv as A,AI_DEFAULT_MODEL_ENV as S,AI_DEFAULT_EMBEDDING_MODEL_ENV as x,AI_PROXY_URL_ENV as b,AI_PROXY_TOKEN_ENV as C,readAiGatewayEnvTags as X,AI_GATEWAY_ID_ENV as z,warnIgnoredBindingToken as Z}from"./AI_DEFAULT_EMBEDDING_MODEL_ENV-IwcHZiKj.mjs";import{m as J,r as O}from"./usage-GPnTSqR8.mjs";const Q={setAttribute:()=>{},setAttributes:()=>{}},G=async(e,t)=>t(G,Q),ee=(e,{metrics:t,trace:a=G})=>{const s={"gen_ai.operation.name":"chat","gen_ai.request.model":e};return{wrapGenerate:async({doGenerate:r})=>a("ai.generate",async(d,l)=>{const o=await r();return O(e,o,l,t),o},s),wrapStream:async({doStream:r})=>{const d=(c,w)=>{const E=c.stream.getReader();let h={};return{...c,stream:new ReadableStream({cancel:async v=>{w({outcome:h}),await E.cancel(v)},pull:async v=>{let f;try{f=await E.read()}catch(y){w({error:y,failed:!0,outcome:h}),v.error(y);return}if(f.done){w({outcome:h}),v.close();return}f.value.type==="finish"&&(h={providerMetadata:f.value.providerMetadata,usage:f.value.usage}),v.enqueue(f.value)}})}},l=Promise.withResolvers(),o=Promise.withResolvers();return a("ai.stream",async(c,w)=>{l.resolve(d(await r(),o.resolve));const E=await o.promise;if(O(e,E.outcome,w,t),E.failed===!0)throw E.error},s).catch(l.reject),l.promise}}},U=(e,t,a)=>{if(t===void 0||typeof e=="string")return e;const s=ee(a??J(e)??"unknown",t);let r;const d=async()=>(r??=import("ai").then(({wrapLanguageModel:o})=>o({middleware:s,model:e}),o=>{throw r=void 0,o}),r),l=e;return{doGenerate:async o=>d().then(c=>c.doGenerate(o)),doStream:async o=>d().then(c=>c.doStream(o)),modelId:l.modelId,provider:l.provider,specificationVersion:"v4",get supportedUrls(){return d().then(o=>o.supportedUrls)}}};let N;const B=()=>(N??=Promise.all([import("@ai-sdk/openai"),import("workers-ai-provider/openai"),import("workers-ai-provider/anthropic")]).then(([e,{openai:t},{anthropic:a}])=>({createOpenAI:e.createOpenAI,plugins:[t,a]}),e=>{throw N=void 0,e}),N),$=(e,t,a)=>({doGenerate:async s=>a().then(r=>r.doGenerate(s)),doStream:async s=>a().then(r=>r.doStream(s)),modelId:e,provider:t,specificationVersion:"v4",get supportedUrls(){return a().then(s=>s.supportedUrls)}}),te={maxEmbeddingsPerCall:3e3,supportsParallelCalls:!0},V=(e,t,a,s)=>({doEmbed:async r=>a().then(d=>d.doEmbed(r)),get maxEmbeddingsPerCall(){return s?.maxEmbeddingsPerCall??a().then(r=>r.maxEmbeddingsPerCall)},modelId:e,provider:t,specificationVersion:"v4",get supportsParallelCalls(){return s?.supportsParallelCalls??a().then(r=>r.supportsParallelCalls)}});let R;const k=()=>(R??=import("workers-ai-provider").catch(e=>{throw R=void 0,e}),R),_=e=>!e.startsWith("@")&&e.includes("/"),re=e=>{const t=(a,s)=>$(a,"workersai.chat",async()=>e(a).then(r=>r(a,s)));return t.textEmbeddingModel=a=>V(a,"workersai.embedding",async()=>e(a).then(s=>s.textEmbeddingModel?.(a)),_(a)?void 0:te),t},I=e=>{throw new g("INTERNAL",`@lunora/ai: ${e} needs the \`AI\` binding (env.AI). In dev, Lunora leaves it out when there is no Cloudflare login: run \`wrangler login\` or set CLOUDFLARE_API_TOKEN. Elsewhere, add an \`ai\` binding to wrangler.jsonc. Or set ${b} to an OpenAI-compatible proxy for "<provider>/<model>" slugs.`)},F=()=>I("this model id");F.textEmbeddingModel=()=>I("this embedding model id");const ae=3040,ne=/^(?:[A-Z]\w*:\s*)?3040\s*:/iu,oe=e=>e?.code===ae||e instanceof Error&&ne.test(e.message),se="default",ie=new Map([[401,"UNAUTHORIZED"],[403,"FORBIDDEN"],[404,"NOT_FOUND"],[408,"SERVICE_UNAVAILABLE"],[429,"RATE_LIMITED"]]),de=e=>ie.get(e)??(e>=400&&e<500?"BAD_REQUEST":"SERVICE_UNAVAILABLE"),ce=300,le=new Set(["127.0.0.1","[::1]","localhost"]),ue=e=>{const t=A(e,b);if(t===void 0)return;const a=A(e,C),s=URL.canParse(t)?new URL(t):void 0;let r;if(s===void 0?r=`${b} is not a valid URL`:a!==void 0&&s.protocol!=="https:"&&!le.has(s.hostname)&&(r=`${b} (${s.origin}) is not HTTPS, so ${C} would travel in cleartext — use an https:// URL`),r!==void 0){const o=()=>{throw new g("INTERNAL",`@lunora/ai: ${r}`)};return{chat:o,embedding:o}}let d;const l=()=>(d??=B().then(({createOpenAI:o})=>o({apiKey:a??"",baseURL:t,name:"lunora-proxy"}),o=>{throw d=void 0,o}),d);return{chat:o=>$(o,"lunora-proxy",async()=>l().then(c=>c.chat(o))),embedding:o=>V(o,"lunora-proxy",async()=>l().then(c=>c.embedding(o)))}},we=(e,t)=>{const a=e===void 0?void 0:X(e);return a===void 0?t:{...t,tags:{...a,...t?.tags}}},he=(e,t,a)=>{const s=W(a);if(e!==void 0)return s!==void 0&&e.metadata===void 0?{...e,metadata:s}:e;if(t===void 0)return;const r=A(t,z);if(r!==void 0)return Z(t),s===void 0?{id:r}:{id:r,metadata:s}},be=e=>{const{binding:t,defaultEmbeddingModel:a,defaultModel:s,env:r,gateway:d,metadata:l,provider:o,telemetry:c}=e,w=ue(r),E=we(r,l),h=he(d,r,E),v=d?.metadata===void 0?W(E):void 0,f=s??A(r,S),y=a??A(r,x);let T;const j=()=>(T??=k().then(({createWorkersAI:n})=>n({binding:t,gateway:h})).catch(n=>{throw T=void 0,n}),T);let M;const H=()=>(M??=Promise.all([k(),B()]).then(([{createWorkersAI:n},{plugins:i}])=>n({binding:t,gateway:h,providers:i})).catch(n=>{throw M=void 0,n}),M),m=o??(t?re(async n=>_(n)?H():j()):F),K=(n,i)=>_(n)?w!==void 0?w.chat(n):v===void 0?m(n):m(n,{metadata:v}):i?.rejectIfBusy===void 0?m(n):m(n,{rejectIfBusy:i.rejectIfBusy}),Y=(n,i)=>{const u=n??f;if(u===void 0||u==="")throw new g("INTERNAL",`@lunora/ai: no model supplied and no default configured — pass a model id, or set ${S} in the Worker env (wrangler \`vars\` / \`.dev.vars\`)`);return typeof u=="string"?U(K(u,i),c,u):U(u,c)},q=n=>{if(w!==void 0&&_(n))return w.embedding(n);const i=m.textEmbeddingModel;if(typeof i!="function")throw new g("INTERNAL","@lunora/ai: the Workers AI provider does not expose `textEmbeddingModel`; pass an AI SDK EmbeddingModel (e.g. from @ai-sdk/openai) to embed()");return i.call(m,n)};return{embeddingModel:n=>{if(typeof n=="object")return n;const i=n??y;if(!i)throw new g("INTERNAL",`@lunora/ai: no embedding model supplied and no default configured — pass an embedding model id or an AI SDK EmbeddingModel, or set ${x} in the Worker env (wrangler \`vars\` / \`.dev.vars\`)`);return q(i)},model:Y,run:async(n,i,u)=>{if(!t)return I("ai.run");const L=h!==void 0&&u?.gateway===void 0?{...u,gateway:h}:u;try{return await t.run(n,i,L)}catch(p){throw oe(p)?new g("RATE_LIMITED",`@lunora/ai: Workers AI has no free capacity for ${n} (error 3040) — retry later`,{cause:p}):p}},websearch:async(n,i)=>{if(!t)return I("ai.websearch");if(typeof t.websearch!="function")throw new g("NOT_IMPLEMENTED","@lunora/ai: this Workers runtime's `AI` binding has no websearch() — update wrangler / @cloudflare/vite-plugin to a release that ships the Web Search API");const{gatewayId:u,...L}=i??{},p=await t.websearch({...L,gatewayId:u??h?.id??se,query:n});if(!p.ok){const P=(await p.text().catch(()=>"")).slice(0,ce);throw new g(de(p.status),`@lunora/ai: web search failed with HTTP ${String(p.status)}${P===""?"":`: ${P}`}`)}const D=await p.json().catch(()=>{});if(!Array.isArray(D?.items))throw new g("INTERNAL","@lunora/ai: web search returned a body that is not JSON with an `items` array");return D},workersai:m}};export{be as default};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import{tool as Re,jsonSchema as De}from"@ai-sdk/provider-utils";import{LunoraError as w,isLunoraError as Ce}from"@lunora/errors";import{stableStringify as Be}from"@lunora/wire";import{r as de,m as L}from"./usage-GPnTSqR8.mjs";import $e from"./fixedWindowChunks-C461ahRE.mjs";import{c as Oe,I as Ke}from"./concurrent-C6nqBv41.mjs";import{contentHash as Ue}from"./contentHash-BIn6ECP8.mjs";import{hybridRank as Qe}from"./hybridRank-B4skyCLx.mjs";import{VECTORIZE_CAPABILITIES as le,vectorizeStore as Ve}from"./VECTORIZE_CAPABILITIES-l9ceRO5j.mjs";const Fe=1e3,Le=200,ze=5,ue=1e3,Pe=4,me=le.maxMetadataBytes===!1?Number.POSITIVE_INFINITY:le.maxMetadataBytes,je=2*1024,we="__ragChunk",be="__ragSource",K="__ragText",q="__ragHash",W="__ragChunks",V="__ragImportance",ve="__ragModel",He=new Set([we,W,q,V,ve,be,K]),Ye=async(e,c,u,m)=>{if(c.length===0)return;const h=c.slice(1);if(!e.upsertMany){await Oe(h,Ke,async(E,A)=>{await e.upsert(E,m),u(A+1)}),await e.upsert(c[0],m),u(0);return}for(let E=0;E<h.length;E+=ue){const A=Math.min(E+ue,h.length);await e.upsertMany(h.slice(E,A),m);for(let U=E;U<A;U+=1)u(U+1)}await e.upsertMany([c[0]],m),u(0)},qe=(e,c,u,m)=>{if(m===!1)return;const h=new TextEncoder().encode(JSON.stringify(e)).length;if(h<=m)return;const A=(typeof e[K]=="string"?new TextEncoder().encode(e[K]).length:0)*2>h?"lower `chunkSize` (it counts characters, not bytes — multibyte text costs up to 3 bytes each), or supply `textStore` to move chunk text out of metadata entirely":"attach less per-source `metadata`";throw new w("BAD_REQUEST",`@lunora/ai/rag: chunk ${String(c)} of "${u}" carries ${String(h)} bytes of metadata, over the store's ${String(m)}-byte per-vector ceiling — ${A}`)},We=(e,c,u)=>{if(u===!1)return;const m=new TextEncoder().encode(e).length;if(!(m<=u))throw new w("BAD_REQUEST",`@lunora/ai/rag: chunk id "${e}" for source "${c}" is ${String(m)} bytes, over the store's ${String(u)}-byte per-vector id ceiling — shorten the source id (hash long keys before indexing them) or shorten the \`namespace\`, which is prefixed onto every chunk id`)},Ze=/^[\w.-]{1,40}$/,ye=e=>e===void 0?"":`${encodeURIComponent(e)}#`,O=(e,c,u)=>`${ye(e)}${c}#${String(u)}`,he=(e,c)=>{const u=ye(c),m=u!==""&&e.startsWith(u)?e.slice(u.length):e,h=m.lastIndexOf("#"),E=h===-1?Number.NaN:Number(m.slice(h+1));return h===-1||!Number.isInteger(E)||E<0?{chunkIndex:0,sourceId:m}:{chunkIndex:E,sourceId:m.slice(0,h)}},Xe=async e=>Ue(new TextEncoder().encode(e)),Je=e=>{try{return Be([e.text,e.metadata,e.importance])}catch{return}},Ge=(e,c)=>{const u=[],m=[];for(const h of e)h.score>=c?u.push(h):m.push(h.id);return{kept:u,rejectedIds:m}},et=e=>e!==void 0&&Object.keys(e).length>0,H=e=>{if(!e)return;const c=Object.entries(e).filter(([u])=>!He.has(u));return c.length>0?Object.fromEntries(c):void 0},pe=new Set,tt=e=>{pe.has(e)||(pe.add(e),console.warn(`[@lunora/ai/rag] index "${e}" is used without a namespace — in a multi-tenant/sharded
|
|
2
|
+
app this shares one tenant's chunks (text included) with every other tenant, since
|
|
3
|
+
Vectorize indexes are account-global. Pass \`namespace\` (the shard/tenant key) on both
|
|
4
|
+
index() and retrieve(). Single-tenant apps suppress this via { allowSharedNamespace: true }.`))},nt=e=>e.map(c=>`[source:${c.sourceId}#${String(c.chunkIndex)}]
|
|
5
|
+
${c.text}`).join(`
|
|
6
|
+
|
|
7
|
+
`),ge=(e,c)=>{if(typeof e=="object")return e;if(c===void 0)throw new w("INTERNAL","@lunora/ai/rag: `embeddingModel` is a Workers AI model id (or omitted) but the bound context has no `ai` (env.AI). Pass an AI SDK EmbeddingModel object to embed without Workers AI, or bind a context whose `ctx.ai` is wired.");return c.embeddingModel(e)},rt=e=>{if(e===void 0)throw new w("INTERNAL","@lunora/ai/rag: the bound context has no `vectors` (env.VECTORIZE) and no `store` is configured — bind a context whose `ctx.vectors` is wired, or configure `store` (e.g. `sqliteVectorStore`) to back this index without Vectorize.");return e};let Y;const fe=async()=>(Y??=import("ai").catch(e=>{throw Y=void 0,e}),Y),pt=e=>{if(typeof e.index!="string"||e.index.length===0)throw new w("BAD_REQUEST","@lunora/ai/rag: `index` must be a non-empty Vectorize index name");const c=e.chunkSize??Fe,u=e.chunkOverlap??Le;if(!Number.isInteger(c)||c<1)throw new w("BAD_REQUEST","@lunora/ai/rag: `chunkSize` must be a positive integer");if(!Number.isInteger(u)||u<0||u>=c)throw new w("BAD_REQUEST","@lunora/ai/rag: `chunkOverlap` must be a non-negative integer smaller than `chunkSize`");const m=me-je;if(!e.chunk&&!e.textStore&&!e.store&&c>m)throw new w("BAD_REQUEST",`@lunora/ai/rag: \`chunkSize\` of ${String(c)} leaves no room under Vectorize's ${String(me)}-byte metadata limit, which also carries this chunk's bookkeeping and any \`metadata\` you attach — keep it under ${String(m)}, or supply \`textStore\` to move chunk text out of metadata entirely`);const h=e.topK??ze;if(!Number.isInteger(h)||h<1)throw new w("BAD_REQUEST","@lunora/ai/rag: `topK` must be a positive integer");if(e.maxEmbeddingDimensions!==void 0&&e.maxEmbeddingDimensions!==!1&&(!Number.isInteger(e.maxEmbeddingDimensions)||e.maxEmbeddingDimensions<1))throw new w("BAD_REQUEST","@lunora/ai/rag: `maxEmbeddingDimensions` must be a positive integer, or `false` to disable the check");if(e.embeddingModelVersion!==void 0&&!Ze.test(e.embeddingModelVersion))throw new w("BAD_REQUEST",'@lunora/ai/rag: `embeddingModelVersion` must match /^[A-Za-z0-9._-]{1,40}$/ (a short, stable tag like "bge-v1.5")');if(e.candidates!==void 0&&(!Number.isInteger(e.candidates)||e.candidates<1))throw new w("BAD_REQUEST","@lunora/ai/rag: `candidates` must be a positive integer");if(e.cacheEmbeddings!==void 0&&(!Number.isInteger(e.cacheEmbeddings)||e.cacheEmbeddings<0))throw new w("BAD_REQUEST","@lunora/ai/rag: `cacheEmbeddings` must be a non-negative integer");const E=e.cacheEmbeddings??0,A=e.rerank,U=e.chunk??(p=>$e(p,c,u)),{textStore:N}=e,Q=e.embeddingModelVersion,z=p=>Q===void 0?p:p===void 0?Q:`${Q}::${p}`;return p=>{const x=e.store?e.store(p):Ve(rt(p.vectors),e.index),Z=N?x.capabilities.maxTopK:x.capabilities.maxTopKWithMetadata,F=e.maxEmbeddingDimensions??x.capabilities.maxDimensions;let B;const X=typeof p.trace=="function"?p.trace:void 0,J=typeof p.metrics?.count=="function"?p.metrics:void 0;let G=F===!1;const ee=(t,n)=>{if(G||(G=!0,F===!1||t<=F))return;const a=L(n);throw new w("BAD_REQUEST",`@lunora/ai/rag: embedding model${a===void 0?"":` "${a}"`} produces ${String(t)}-dimension vectors, over the ${String(F)}-dimension ceiling of index "${e.index}" — either truncate them with the provider's \`dimensions\` option (Matryoshka models such as text-embedding-3-large support this), or set \`maxEmbeddingDimensions: false\` if this index is not Vectorize-backed`)},$=new Map,Ee=(t,n)=>{if(E!==0)for($.set(t,n);$.size>E;){const a=$.keys().next();if(a.done===!0)break;$.delete(a.value)}},te=async t=>{const n=$.get(t);if(n!==void 0)return n;B??=ge(e.embeddingModel,p.ai);const a=B,i=async r=>{const l=await fe(),{embedding:g,providerMetadata:d,usage:v}=await l.embed({model:a,value:t});return ee(g.length,a),de(L(a),{providerMetadata:d,usage:{inputTokens:{total:v.tokens}}},r,J),Ee(t,g),g};if(X===void 0)return i();const s=L(a),o=typeof p.conversationId=="string"&&p.conversationId.length>0?p.conversationId:void 0;return X("ai.embed",(r,l)=>i(l),{"gen_ai.operation.name":"embeddings",...s===void 0?{}:{"gen_ai.request.model":s},...o===void 0?{}:{"gen_ai.conversation.id":o}})},xe=async(t,n,a)=>{if(!e.transformQuery||n?.transformQuery===!1)return[t];const i=typeof p.conversationId=="string"&&p.conversationId.length>0?p.conversationId:void 0,s=await e.transformQuery(t,{conversationId:i,namespace:a}),o=(typeof s=="string"?[s]:[...s]).map(r=>r.trim()).filter(r=>r.length>0);return o.length>0?o:[t]},Ie=async t=>{const n=new Map,a=[...new Set(t.filter(i=>!$.has(i)))];if(a.length<2)return n;B??=ge(e.embeddingModel,p.ai);try{const i=await fe(),{embeddings:s,providerMetadata:o,usage:r}=await i.embedMany({model:B,values:a});if(de(L(B),{providerMetadata:o,usage:{inputTokens:{total:r.tokens}}},void 0,J),s.length!==a.length)return n;const[l]=s;l!==void 0&&ee(l.length,B);for(const[g,d]of a.entries())n.set(d,s[g])}catch(i){if(Ce(i))throw i}return n},P=t=>{if(t===void 0){if(e.requireNamespace)throw new w("BAD_REQUEST",`@lunora/ai/rag: index "${e.index}" requires a namespace (requireNamespace is set) — pass the tenant/shard key on index()/retrieve()/remove()`);e.allowSharedNamespace||tt(e.index)}},ne=async(t,n)=>{const[a]=await x.getByIds([O(n,t,0)],n),i=a?.metadata?.[q],s=a?.metadata?.[W];return{chunks:typeof s=="number"&&Number.isInteger(s)&&s>0?s:void 0,hash:typeof i=="string"?i:void 0}},re=async(t,n,a,i,s)=>{const o=Array.from({length:a-n},(l,g)=>O(i,t,n+g));if(o.length===0)return;const r=async()=>{await N?.remove?.(o,{namespace:i}),await e.lexicalStore?.remove?.(o,{namespace:i})};if(s===void 0||!x.capabilities.deferredDelete){await x.deleteByIds(o,i),await r();return}await x.deleteByIds(o,i,s,r)},Se=async t=>{if(P(t.namespace),t.importance!==void 0&&(typeof t.importance!="number"||t.importance<0||t.importance>1))throw new w("BAD_REQUEST","@lunora/ai/rag: `importance` must be a number in [0, 1]");const n=z(t.namespace),a=Je(t),i=await Xe(a??t.text),s=await ne(t.id,n);if(t.reindex!==!0&&a!==void 0&&s.hash===i&&s.chunks!==void 0)return{chunks:s.chunks,ids:Array.from({length:s.chunks},(b,S)=>O(n,t.id,S)),unchanged:!0};const o=U(t.text),r=o.map((b,S)=>O(n,t.id,S)),l=r.at(-1);if(l!==void 0&&We(l,t.id,x.capabilities.maxIdBytes),o.length===0&&t.allowEmptySources===!1)throw new w("BAD_REQUEST",`@lunora/ai/rag: source "${t.id}" produced zero chunks — set allowEmptySources: true to allow this`);if(o.length>0){const b=o.map((S,k)=>({chunkIndex:k,id:r[k],sourceId:t.id,text:S,...t.metadata===void 0?{}:{metadata:t.metadata}}));N&&await N.put(b,{namespace:n}),e.lexicalStore&&await e.lexicalStore.index(b,{namespace:n})}const g=await Ie(o),d=async b=>g.get(b)??await te(b),v=o.map((b,S)=>{const k=r[S],T={...t.metadata,[we]:S,[be]:t.id};return N||(T[K]=b),t.importance!==void 0&&(T[V]=t.importance),S===0&&(T[q]=i,T[W]=o.length,Q!==void 0&&(T[ve]=Q)),qe(T,S,t.id,x.capabilities.maxMetadataBytes),{embed:d,id:k,input:b,metadata:T,namespace:n}}),y=b=>{t.onChunk?.({chunkIndex:b,id:r[b],text:o[b],total:o.length})},I={};return await Ye(x,v,y,I),s.chunks!==void 0&&s.chunks>o.length&&await re(t.id,o.length,s.chunks,n,I),{chunks:o.length,ids:r,unchanged:!1}},ke=async t=>{P(t.namespace);const n=z(t.namespace),i=(await ne(t.id,n)).chunks??1;await re(t.id,0,i,n)},ae=async(t,n)=>{const a=new Map;if(t.length===0)return a;if(N){const s=await N.getMany(t,{namespace:n});for(const[o,r]of t.entries()){const l=s[o];typeof l=="string"&&a.set(r,l)}return a}const i=await x.getByIds(t,n);for(const s of i){const o=s.metadata?.[K];typeof o=="string"&&a.set(s.id,o)}return a},Te=async(t,n,a)=>{const i=n?.chunkContext?.before??0,s=n?.chunkContext?.after??0;if(i===0&&s===0)return t;if(!Number.isInteger(i)||i<0||!Number.isInteger(s)||s<0)throw new w("BAD_REQUEST","@lunora/ai/rag: `chunkContext.before`/`chunkContext.after` must be non-negative integers");const o=new Map(t.map(d=>[d.id,d.text])),r=new Set;for(const d of t)for(let v=-i;v<=s;v+=1){const y=d.chunkIndex+v,I=O(a,d.sourceId,y);v!==0&&y>=0&&!o.has(I)&&r.add(I)}const l=await ae([...r],a),g=(d,v)=>{const y=O(a,d,v);return o.get(y)??l.get(y)};return t.map(d=>{const v=[];for(let y=-i;y<=s;y+=1){const I=y===0?d.text:g(d.sourceId,d.chunkIndex+y);I!==void 0&&v.push(I)}return{...d,text:v.join(`
|
|
8
|
+
`)}})},_e=t=>{if(typeof t=="string"){const n=e.filters!==void 0&&Object.hasOwn(e.filters,t)?e.filters[t]:void 0;if(!n)throw new w("NOT_FOUND",`@lunora/ai/rag: unknown named filter "${t}" — must be one of the keys declared in RagConfig.filters`);return n.filter}return t},Me=(t,n)=>t.matches.map(a=>{const i=a.metadata??{},s=he(a.id,n),o=i[K],r=i[V],l=typeof r=="number"&&r>=0&&r<=1?r:1;return{chunkIndex:s.chunkIndex,id:a.id,importance:l,metadata:H(i),score:a.score*l,sourceId:s.sourceId,text:typeof o=="string"?o:""}}),Ae=async(t,n)=>{if(!N)return t;const a=t.map(r=>r.id),[i,s]=await Promise.all([ae(a,n),x.getByIds(a,n)]),o=new Map(s.map(r=>[r.id,r.metadata]));return t.flatMap(r=>{const l=i.get(r.id);if(l===void 0)return[];const g=o.get(r.id),d=g?.[V],v=typeof d=="number"&&d>=0&&d<=1?d:r.importance,I=(r.importance===0?0:r.score/r.importance)*v;return[{...r,importance:v,metadata:H(g)??r.metadata,score:I,text:l}]})},se=async(t,n,a)=>{const i=t.map(r=>r.id).filter(r=>!n.has(r)),s=i.length===0?[]:await x.getByIds(i,a),o=new Map(s.map(r=>[r.id,r.metadata]));return t.map(r=>{const l=he(r.id,a),g=o.get(r.id),d=g?.[V];return{chunkIndex:l.chunkIndex,id:r.id,importance:typeof d=="number"&&d>=0&&d<=1?d:1,metadata:H(g),score:r.score,sourceId:l.sourceId,text:r.text}})},oe=async(t,n)=>{P(n?.namespace);const a=z(n?.namespace),i=_e(n?.filter),s=e.rlsFilter?await e.rlsFilter(p.auth):void 0,o=s?{...i,...s}:i,r=Math.min(n?.topK??h,Z),l=await xe(t,n,a),g=l[0],d=A!==void 0&&n?.rerank!==!1,y=d||e.lexicalStore!==void 0||e.graphStore!==void 0||l.length>1?Math.min(e.candidates??r*Pe,Z):r,I=n?.minScore,b=new Set,S=async f=>{const R=await x.query({embed:te,filter:o,input:f,namespace:a,returnMetadata:N?"indexed":"all",topK:y}),C=await Ae(Me(R,a),a);if(I===void 0)return C;const{kept:M,rejectedIds:D}=Ge(C,I);for(const Ne of D)b.add(Ne);return M},k=[{chunks:await S(g)}];for(const f of l.slice(1))k.push({chunks:await S(f)});if(e.lexicalStore){const f=await e.lexicalStore.search(g,{filter:o,namespace:a,topK:e.lexicalTopK??y}),R=new Set(k.flatMap(M=>M.chunks).map(M=>M.id)),C=f.filter(M=>!b.has(M.id)||R.has(M.id));k.push({chunks:await se(C,R,a)})}const T=k.flatMap(f=>[...f.chunks]);if(e.graphStore&&T.length>0&&(e.graphStore.enforcesFilter||!et(o))){const f=[...new Set(T.map(D=>D.sourceId))],R=await e.graphStore.related(f,{filter:o,namespace:a,topK:e.graphTopK??y}),C=new Set(T.map(D=>D.id)),M=R.filter(D=>!b.has(D.id)||C.has(D.id));k.push({chunks:await se(M,C,a),weight:"proximity"})}const j=k.filter(f=>f.chunks.length>0);let _=j.length>1?[...Qe(j)]:[...j[0]?.chunks??[]];_.sort((f,R)=>R.score-f.score),d&&(_=[...await A(g,_)]),_=_.slice(0,r),_=[...await Te(_,n,a)];const ie=[],ce=new Set;for(const f of _)ce.has(f.sourceId)||(ce.add(f.sourceId),ie.push({id:f.sourceId,metadata:f.metadata,weight:f.importance}));return n?.onRetrieve?.({matches:_.length,query:t}),{chunks:_,context:nt(_),sources:ie}};return{asTool:t=>Re({description:t?.description??`Search the "${e.index}" knowledge base for passages relevant to a natural-language query.`,execute:async({query:n})=>oe(n,{namespace:t?.namespace,topK:t?.topK}),inputSchema:De({properties:{query:{description:"The natural-language search query.",type:"string"}},required:["query"],type:"object"})}),index:Se,remove:ke,retrieve:oe}}};export{pt as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{stableStringify as l}from"@lunora/wire";const v=c=>{const m=c.delayMs??0,y=(d,a)=>c.id===void 0?a:c.id(d),e=d=>d===void 0?void 0:c.text(d),u=d=>c.metadata?.(d),p=d=>c.namespace?.(d),o=(d,a)=>{const t=p(d);return{id:y(d,a),...t===void 0?{}:{namespace:t}}},f=(d,a,t)=>{const i=u(d);return{...o(d,a),...i===void 0?{}:{metadata:i},text:t}},g=(d,a)=>{try{return l(u(d))===l(u(a))}catch{return!1}},r=async(d,a)=>{await d.scheduler.runAfter(m,c.action,a)};return{afterDelete:async(d,a)=>{const t=a.previous??a.doc;await r(d,{deleted:!0,...t===void 0?{id:a.id}:o(t,a.id)})},afterInsert:async(d,a)=>{const t=e(a.doc);t===void 0||a.doc===void 0||await r(d,f(a.doc,a.id,t))},afterUpdate:async(d,a)=>{if(a.doc===void 0)return;const t=e(a.doc),i=e(a.previous),s=o(a.doc,a.id),n=a.previous===void 0?s:o(a.previous,a.id),w=n.id!==s.id||n.namespace!==s.namespace,h=c.metadata!==void 0&&a.previous!==void 0&&!g(a.doc,a.previous);if(w)await r(d,{deleted:!0,...n});else if(t===i&&!h)return;await(t===void 0?r(d,{deleted:!0,...s}):r(d,f(a.doc,a.id,t)))}}};export{v as ragSyncTriggers};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import x from"./matchesMetadataFilter-BbIOyA5g.mjs";import{p as I,a as q,r as y,c as F,i as A}from"./sql-D5aqEMCY.mjs";import{LunoraError as _}from"@lunora/errors";const O=500,C=4096,R=(d,n)=>JSON.stringify([d,n]),D=d=>{const{dimensions:n,exec:r,table:l}=d,i=`${l}_ann`,T=`${l}_ann_ready`,u=(a,t)=>{if(a!==n)throw new _("RAG_DIMENSION_MISMATCH",`@lunora/ai/rag: ${t} is ${String(a)}-dimension but the \`ann\` index is ${String(n)}-dimension — set \`ann.dimensions\` to the embedding model's width, or reindex into a new \`table\` after changing models`)},g=async(a,t,o)=>{await r(`INSERT INTO ${i} (ref, namespace, embedding, id) VALUES (?, ?, ?, ?)`,[R(a,t),a,o,t])},S=async()=>{let a=0;for(;;){const t=await r(`SELECT rowid, id, namespace, vector FROM ${l} WHERE rowid > ? ORDER BY rowid LIMIT ?`,[a,O]);for(const o of t)u(JSON.parse(String(o.vector)).length,`stored vector "${String(o.id)}"`),await g(String(o.namespace),String(o.id),String(o.vector));if(t.length<O)return;a=Number(t.at(-1)?.rowid)}};return{ensure:async()=>{if((await r("SELECT name FROM sqlite_master WHERE name = ?",[T])).length>0){const[t]=await r(`SELECT dimensions FROM ${T}`,[]);u(Number(t?.dimensions),"the existing index");return}await r(`DROP TABLE IF EXISTS ${i}`,[]);try{await r(`CREATE VIRTUAL TABLE ${i} USING vec0(ref TEXT PRIMARY KEY, namespace TEXT PARTITION KEY, embedding FLOAT[${String(n)}] distance_metric=cosine, +id TEXT)`,[])}catch(t){throw t instanceof Error&&t.message.includes("no such module: vec0")?new Error("@lunora/ai/rag: sqliteVectorStore `ann` needs the sqlite-vec extension in this SQLite (`no such module: vec0`) — on celld set the `sqlite_vec` compatibility flag, on node:sqlite load the extension",{cause:t}):t}await S(),await r(`CREATE TABLE ${T} (dimensions INTEGER NOT NULL)`,[]),await r(`INSERT INTO ${T} (dimensions) VALUES (?)`,[n])},nearest:async(a,t,o)=>(u(t.length,"the query embedding"),(await r(`SELECT id, distance FROM ${i} WHERE embedding MATCH ? AND k = ? AND namespace = ?`,[JSON.stringify([...t]),Math.min(o,C),a])).map(e=>({distance:Number(e.distance),id:String(e.id)}))),put:async(a,t,o)=>{u(o.length,`the embedding for "${t}"`),await r(`DELETE FROM ${i} WHERE ref = ?`,[R(a,t)]),await g(a,t,JSON.stringify([...o]))},remove:async(a,t)=>{await r(`DELETE FROM ${i} WHERE ref IN (${I(t.length)})`,t.map(o=>R(a,o)))}}},B="lunora_rag_vectors",U=5e4,$=100,V=null,h=d=>d??"",W=d=>{if(typeof d.exec!="function")throw new TypeError("@lunora/ai/rag: sqliteVectorStore requires an `exec` function");const n=q(d.table??B,"sqliteVectorStore `table`"),r=d.maxScan??U,{ann:l,exec:i}=d;if(l!==void 0&&(!Number.isInteger(l.dimensions)||l.dimensions<=0))throw new TypeError("@lunora/ai/rag: sqliteVectorStore `ann.dimensions` must be a positive integer");const T=l===void 0?void 0:D({dimensions:l.dimensions,exec:i,table:n}),u={deferredDelete:!0,maxDimensions:d.maxDimensions??l?.dimensions??!1,maxIdBytes:!1,maxMetadataBytes:!1,maxTopK:$,maxTopKWithMetadata:$};let g;const S=async()=>{g??=(async()=>{await i(`CREATE TABLE IF NOT EXISTS ${n} (id TEXT NOT NULL, namespace TEXT NOT NULL DEFAULT '', vector TEXT NOT NULL, metadata TEXT, PRIMARY KEY (namespace, id))`,[]),await i(`CREATE INDEX IF NOT EXISTS ${n}_namespace ON ${n} (namespace)`,[]),await T?.ensure()})().catch(e=>{throw g=void 0,e}),await g},b=async e=>{if(await S(),!e.embed)throw new TypeError("@lunora/ai/rag: sqliteVectorStore requires an `embed` function on upsert");const c=await e.embed(e.input),s=h(e.namespace);await T?.put(s,e.id,c),await i(`INSERT INTO ${n} (id, namespace, vector, metadata) VALUES (${I(4)}) ON CONFLICT(namespace, id) DO UPDATE SET vector = excluded.vector, metadata = excluded.metadata`,[e.id,s,JSON.stringify([...c]),e.metadata===void 0?V:JSON.stringify(e.metadata)])},a=async(e,c)=>{if(await S(),e.length===0)return[];const s=[];for(const m of A(e)){const E=await i(`SELECT id, metadata FROM ${n} WHERE namespace = ? AND id IN (${I(m.length)})`,[h(c),...m]);for(const w of E){const f=y(w.metadata);s.push({id:String(w.id),...f===void 0?{}:{metadata:f}})}}return s},t=async(e,c,s)=>{const m=h(s.namespace),E=await e.nearest(m,c,s.topK??10),w=await a(E.map(L=>L.id),m),f=new Map(w.map(L=>[L.id,L])),N=[];for(const{distance:L,id:p}of E){const v=f.get(p);v!==void 0&&N.push({id:p,score:1-L,...s.returnMetadata==="none"||v.metadata===void 0?{}:{metadata:v.metadata}})}return{count:N.length,matches:N}};return{capabilities:u,deleteByIds:async(e,c,s,m)=>{if(await S(),e.length===0){await m?.();return}for(const E of A(e))await i(`DELETE FROM ${n} WHERE namespace = ? AND id IN (${I(E.length)})`,[h(c),...E]),await T?.remove(h(c),E);await m?.()},getByIds:a,query:async e=>{await S();let c;if(e.embed&&e.input!==void 0)c=await e.embed(e.input);else throw new TypeError("@lunora/ai/rag: sqliteVectorStore query requires both `input` and `embed`");if(T!==void 0&&e.filter===void 0)return t(T,c,e);const s=await i(`SELECT id, vector, metadata FROM ${n} WHERE namespace = ? LIMIT ?`,[h(e.namespace),r+1]);if(s.length>r)throw new RangeError(`@lunora/ai/rag: sqliteVectorStore scanned ${String(s.length)} vectors in namespace "${h(e.namespace)}", over the ${String(r)} limit — search here is brute force and linear, so this namespace has outgrown it. Shard it further, or move this index to Vectorize or a pgvector backend`);const m=[];for(const w of s){const f=y(w.metadata);if(!x(f,e.filter))continue;const N=y(w.vector);N!==void 0&&m.push({id:String(w.id),score:F(c,N),...e.returnMetadata==="none"||f===void 0?{}:{metadata:f}})}const E=m.toSorted((w,f)=>f.score-w.score).slice(0,e.topK??10);return{count:E.length,matches:E}},upsert:b}};export{W as sqliteVectorStore};
|
|
@@ -203,7 +203,9 @@ interface AiBindingLike {
|
|
|
203
203
|
* Calling it with a model id yields an AI SDK {@link LanguageModel}; the
|
|
204
204
|
* optional `textEmbeddingModel` factory yields an {@link EmbeddingModel}.
|
|
205
205
|
* Typed structurally so `@lunora/ai` neither re-declares the provider's full
|
|
206
|
-
* surface nor hard-pins its exact type across minor releases.
|
|
206
|
+
* surface nor hard-pins its exact type across minor releases. It is the shape of
|
|
207
|
+
* the lazy delegate `ctx.ai.workersai` exposes, so only the callable factory and
|
|
208
|
+
* `textEmbeddingModel` are part of it.
|
|
207
209
|
* @experimental
|
|
208
210
|
*/
|
|
209
211
|
interface WorkersAiProviderLike {
|
|
@@ -439,7 +441,7 @@ type EmbeddingModelInput = EmbeddingModel | string;
|
|
|
439
441
|
* so both accept Workers AI and bring-your-own providers. Feed the resolved model
|
|
440
442
|
* to the AI SDK functions re-exported from `@lunora/ai` (`generateText`,
|
|
441
443
|
* `streamText`, `generateObject`, `embed`, …); `run` is the raw binding escape
|
|
442
|
-
* hatch, and `workersai` is the
|
|
444
|
+
* hatch, and `workersai` is the lazy provider delegate for direct model access.
|
|
443
445
|
* @experimental
|
|
444
446
|
*/
|
|
445
447
|
interface LunoraAi {
|
|
@@ -467,7 +469,14 @@ interface LunoraAi {
|
|
|
467
469
|
* binding was supplied, or if the runtime's binding has no `websearch()`.
|
|
468
470
|
*/
|
|
469
471
|
websearch: (query: string, options?: AiWebSearchOptions) => Promise<AiWebSearchResult>;
|
|
470
|
-
/**
|
|
472
|
+
/**
|
|
473
|
+
* A lazy delegate to the Workers AI provider: call it as
|
|
474
|
+
* `ai.workersai("@cf/…")` for a raw language model, or use
|
|
475
|
+
* `ai.workersai.textEmbeddingModel("@cf/…")` for embeddings. The provider
|
|
476
|
+
* module loads on first use, so only the callable factory and
|
|
477
|
+
* `textEmbeddingModel` are exposed — the rest of the `createWorkersAI`
|
|
478
|
+
* factories (image, transcription, speech, …) are not part of this surface.
|
|
479
|
+
*/
|
|
471
480
|
workersai: WorkersAiProviderLike;
|
|
472
481
|
}
|
|
473
|
-
export {
|
|
482
|
+
export { AiBindingLike as A, EmbeddingModelInput as E, LunoraAi as L, ModelInput as M, ResolvedAiGateway as R, WorkersAiProviderLike as W, LunoraAiOptions as a, AiWebSearchItem as b, AiWebSearchProvider as c, AI_DEFAULT_EMBEDDING_MODEL_ENV as d, AI_DEFAULT_MODEL_ENV as e, AI_GATEWAY_ACCOUNT_ID_ENV as f, AI_GATEWAY_ID_ENV as g, AI_GATEWAY_METADATA_MAX_KEYS as h, AI_GATEWAY_TAGS_ENV as i, AI_GATEWAY_TOKEN_ENV as j, AI_PROXY_TOKEN_ENV as k, AI_PROXY_URL_ENV as l, AiGatewayMetadata as m, AiGatewayOptions as n, AiMetrics as o, AiModelOptions as p, AiRunOptions as q, AiSpan as r, AiTelemetry as s, AiTracer as t, AiWebSearchOptions as u, AiWebSearchResult as v, buildAiGatewayMetadataFields as w, readAiGatewayEnvTags as x, resolveAiGateway as y };
|
|
@@ -203,7 +203,9 @@ interface AiBindingLike {
|
|
|
203
203
|
* Calling it with a model id yields an AI SDK {@link LanguageModel}; the
|
|
204
204
|
* optional `textEmbeddingModel` factory yields an {@link EmbeddingModel}.
|
|
205
205
|
* Typed structurally so `@lunora/ai` neither re-declares the provider's full
|
|
206
|
-
* surface nor hard-pins its exact type across minor releases.
|
|
206
|
+
* surface nor hard-pins its exact type across minor releases. It is the shape of
|
|
207
|
+
* the lazy delegate `ctx.ai.workersai` exposes, so only the callable factory and
|
|
208
|
+
* `textEmbeddingModel` are part of it.
|
|
207
209
|
* @experimental
|
|
208
210
|
*/
|
|
209
211
|
interface WorkersAiProviderLike {
|
|
@@ -439,7 +441,7 @@ type EmbeddingModelInput = EmbeddingModel | string;
|
|
|
439
441
|
* so both accept Workers AI and bring-your-own providers. Feed the resolved model
|
|
440
442
|
* to the AI SDK functions re-exported from `@lunora/ai` (`generateText`,
|
|
441
443
|
* `streamText`, `generateObject`, `embed`, …); `run` is the raw binding escape
|
|
442
|
-
* hatch, and `workersai` is the
|
|
444
|
+
* hatch, and `workersai` is the lazy provider delegate for direct model access.
|
|
443
445
|
* @experimental
|
|
444
446
|
*/
|
|
445
447
|
interface LunoraAi {
|
|
@@ -467,7 +469,14 @@ interface LunoraAi {
|
|
|
467
469
|
* binding was supplied, or if the runtime's binding has no `websearch()`.
|
|
468
470
|
*/
|
|
469
471
|
websearch: (query: string, options?: AiWebSearchOptions) => Promise<AiWebSearchResult>;
|
|
470
|
-
/**
|
|
472
|
+
/**
|
|
473
|
+
* A lazy delegate to the Workers AI provider: call it as
|
|
474
|
+
* `ai.workersai("@cf/…")` for a raw language model, or use
|
|
475
|
+
* `ai.workersai.textEmbeddingModel("@cf/…")` for embeddings. The provider
|
|
476
|
+
* module loads on first use, so only the callable factory and
|
|
477
|
+
* `textEmbeddingModel` are exposed — the rest of the `createWorkersAI`
|
|
478
|
+
* factories (image, transcription, speech, …) are not part of this surface.
|
|
479
|
+
*/
|
|
471
480
|
workersai: WorkersAiProviderLike;
|
|
472
481
|
}
|
|
473
|
-
export {
|
|
482
|
+
export { AiBindingLike as A, EmbeddingModelInput as E, LunoraAi as L, ModelInput as M, ResolvedAiGateway as R, WorkersAiProviderLike as W, LunoraAiOptions as a, AiWebSearchItem as b, AiWebSearchProvider as c, AI_DEFAULT_EMBEDDING_MODEL_ENV as d, AI_DEFAULT_MODEL_ENV as e, AI_GATEWAY_ACCOUNT_ID_ENV as f, AI_GATEWAY_ID_ENV as g, AI_GATEWAY_METADATA_MAX_KEYS as h, AI_GATEWAY_TAGS_ENV as i, AI_GATEWAY_TOKEN_ENV as j, AI_PROXY_TOKEN_ENV as k, AI_PROXY_URL_ENV as l, AiGatewayMetadata as m, AiGatewayOptions as n, AiMetrics as o, AiModelOptions as p, AiRunOptions as q, AiSpan as r, AiTelemetry as s, AiTracer as t, AiWebSearchOptions as u, AiWebSearchResult as v, buildAiGatewayMetadataFields as w, readAiGatewayEnvTags as x, resolveAiGateway as y };
|
package/dist/rag/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Tool } from 'ai';
|
|
2
|
-
import { E as EmbeddingModelInput,
|
|
2
|
+
import { E as EmbeddingModelInput, L as LunoraAi } from "../packem_shared/types.d-DL0j0z-W.mjs";
|
|
3
3
|
/**
|
|
4
4
|
* Built-in fixed-window chunker: split into `size`-char windows overlapping by
|
|
5
5
|
* `overlap` chars. Deliberately simple and deterministic — the zero-config
|
|
@@ -73,6 +73,14 @@ declare const tokenChunker: (options: TokenChunkerOptions) => ((text: string) =>
|
|
|
73
73
|
* rather than at the backend with nothing saying why.
|
|
74
74
|
*/
|
|
75
75
|
interface RagVectorStoreCapabilities {
|
|
76
|
+
/**
|
|
77
|
+
* Whether `deleteByIds` honours its `operation` and `afterDelete` arguments:
|
|
78
|
+
* the delete may be deferred with the operation's other writes and skipped if
|
|
79
|
+
* one failed, and `afterDelete` runs when the delete does. Absent means the
|
|
80
|
+
* store only implements `deleteByIds(ids, namespace)`, and `defineRag` removes
|
|
81
|
+
* the chunk text itself, inline, after the delete.
|
|
82
|
+
*/
|
|
83
|
+
deferredDelete?: boolean;
|
|
76
84
|
/**
|
|
77
85
|
* Ceiling on embedding dimensionality, or `false` for no limit.
|
|
78
86
|
*
|
|
@@ -111,10 +119,14 @@ interface RagVectorStoreCapabilities {
|
|
|
111
119
|
*/
|
|
112
120
|
interface RagVectorStore {
|
|
113
121
|
capabilities: RagVectorStoreCapabilities;
|
|
114
|
-
|
|
122
|
+
/** `afterDelete`, when given, must run once the delete has actually happened (a deferred store runs it with the delete). */
|
|
123
|
+
deleteByIds: (ids: ReadonlyArray<string>, namespace?: string, operation?: object, afterDelete?: () => Promise<void>) => Promise<unknown>;
|
|
115
124
|
getByIds: (ids: ReadonlyArray<string>, namespace?: string) => Promise<ReadonlyArray<RagVectorRecord>>;
|
|
116
125
|
query: (input: RagVectorQueryInput) => Promise<RagVectorMatches>;
|
|
117
|
-
|
|
126
|
+
/** `operation` is one `index()` call's token, passed to every write of that call. */
|
|
127
|
+
upsert: (input: RagVectorUpsertInput, operation?: object) => Promise<unknown>;
|
|
128
|
+
/** Batch write of at most 1000 inputs. Optional: without it `defineRag` writes per chunk via `upsert`. */
|
|
129
|
+
upsertMany?: (inputs: ReadonlyArray<RagVectorUpsertInput>, operation?: object) => Promise<unknown>;
|
|
118
130
|
}
|
|
119
131
|
/**
|
|
120
132
|
* Vectorize's documented limits.
|
|
@@ -202,10 +214,15 @@ interface RagVectorUpsertInput {
|
|
|
202
214
|
* @experimental
|
|
203
215
|
*/
|
|
204
216
|
interface RagVectors {
|
|
205
|
-
deleteByIds
|
|
217
|
+
/** Set by a facade whose `deleteByIds` honours `operation` and `afterDelete`; see `RagVectorStoreCapabilities.deferredDelete`. */
|
|
218
|
+
deferredDelete?: boolean;
|
|
219
|
+
deleteByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string, operation?: object, afterDelete?: () => Promise<void>) => Promise<unknown>;
|
|
206
220
|
getByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string) => Promise<ReadonlyArray<RagVectorRecord>>;
|
|
207
221
|
query: (indexName: string, input: RagVectorQueryInput) => Promise<RagVectorMatches>;
|
|
208
|
-
|
|
222
|
+
/** `operation` groups the writes of one logical operation: a deferred facade skips the rest once one fails. */
|
|
223
|
+
upsert: (indexName: string, input: RagVectorUpsertInput, operation?: object) => Promise<unknown>;
|
|
224
|
+
/** Batch write of at most 1000 inputs. Optional: without it `defineRag` writes per chunk via `upsert`. */
|
|
225
|
+
upsertMany?: (indexName: string, inputs: ReadonlyArray<RagVectorUpsertInput>, operation?: object) => Promise<unknown>;
|
|
209
226
|
}
|
|
210
227
|
/**
|
|
211
228
|
* The two facades `defineRag` binds. An `ActionCtx` satisfies this directly
|
package/dist/rag/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Tool } from 'ai';
|
|
2
|
-
import { E as EmbeddingModelInput,
|
|
2
|
+
import { E as EmbeddingModelInput, L as LunoraAi } from "../packem_shared/types.d-DL0j0z-W.js";
|
|
3
3
|
/**
|
|
4
4
|
* Built-in fixed-window chunker: split into `size`-char windows overlapping by
|
|
5
5
|
* `overlap` chars. Deliberately simple and deterministic — the zero-config
|
|
@@ -73,6 +73,14 @@ declare const tokenChunker: (options: TokenChunkerOptions) => ((text: string) =>
|
|
|
73
73
|
* rather than at the backend with nothing saying why.
|
|
74
74
|
*/
|
|
75
75
|
interface RagVectorStoreCapabilities {
|
|
76
|
+
/**
|
|
77
|
+
* Whether `deleteByIds` honours its `operation` and `afterDelete` arguments:
|
|
78
|
+
* the delete may be deferred with the operation's other writes and skipped if
|
|
79
|
+
* one failed, and `afterDelete` runs when the delete does. Absent means the
|
|
80
|
+
* store only implements `deleteByIds(ids, namespace)`, and `defineRag` removes
|
|
81
|
+
* the chunk text itself, inline, after the delete.
|
|
82
|
+
*/
|
|
83
|
+
deferredDelete?: boolean;
|
|
76
84
|
/**
|
|
77
85
|
* Ceiling on embedding dimensionality, or `false` for no limit.
|
|
78
86
|
*
|
|
@@ -111,10 +119,14 @@ interface RagVectorStoreCapabilities {
|
|
|
111
119
|
*/
|
|
112
120
|
interface RagVectorStore {
|
|
113
121
|
capabilities: RagVectorStoreCapabilities;
|
|
114
|
-
|
|
122
|
+
/** `afterDelete`, when given, must run once the delete has actually happened (a deferred store runs it with the delete). */
|
|
123
|
+
deleteByIds: (ids: ReadonlyArray<string>, namespace?: string, operation?: object, afterDelete?: () => Promise<void>) => Promise<unknown>;
|
|
115
124
|
getByIds: (ids: ReadonlyArray<string>, namespace?: string) => Promise<ReadonlyArray<RagVectorRecord>>;
|
|
116
125
|
query: (input: RagVectorQueryInput) => Promise<RagVectorMatches>;
|
|
117
|
-
|
|
126
|
+
/** `operation` is one `index()` call's token, passed to every write of that call. */
|
|
127
|
+
upsert: (input: RagVectorUpsertInput, operation?: object) => Promise<unknown>;
|
|
128
|
+
/** Batch write of at most 1000 inputs. Optional: without it `defineRag` writes per chunk via `upsert`. */
|
|
129
|
+
upsertMany?: (inputs: ReadonlyArray<RagVectorUpsertInput>, operation?: object) => Promise<unknown>;
|
|
118
130
|
}
|
|
119
131
|
/**
|
|
120
132
|
* Vectorize's documented limits.
|
|
@@ -202,10 +214,15 @@ interface RagVectorUpsertInput {
|
|
|
202
214
|
* @experimental
|
|
203
215
|
*/
|
|
204
216
|
interface RagVectors {
|
|
205
|
-
deleteByIds
|
|
217
|
+
/** Set by a facade whose `deleteByIds` honours `operation` and `afterDelete`; see `RagVectorStoreCapabilities.deferredDelete`. */
|
|
218
|
+
deferredDelete?: boolean;
|
|
219
|
+
deleteByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string, operation?: object, afterDelete?: () => Promise<void>) => Promise<unknown>;
|
|
206
220
|
getByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string) => Promise<ReadonlyArray<RagVectorRecord>>;
|
|
207
221
|
query: (indexName: string, input: RagVectorQueryInput) => Promise<RagVectorMatches>;
|
|
208
|
-
|
|
222
|
+
/** `operation` groups the writes of one logical operation: a deferred facade skips the rest once one fails. */
|
|
223
|
+
upsert: (indexName: string, input: RagVectorUpsertInput, operation?: object) => Promise<unknown>;
|
|
224
|
+
/** Batch write of at most 1000 inputs. Optional: without it `defineRag` writes per chunk via `upsert`. */
|
|
225
|
+
upsertMany?: (indexName: string, inputs: ReadonlyArray<RagVectorUpsertInput>, operation?: object) => Promise<unknown>;
|
|
209
226
|
}
|
|
210
227
|
/**
|
|
211
228
|
* The two facades `defineRag` binds. An `ActionCtx` satisfies this directly
|
package/dist/rag/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{default as o}from"../packem_shared/fixedWindowChunks-C461ahRE.mjs";import{markdownChunker as a,sentenceChunker as f,tokenChunker as n}from"../packem_shared/markdownChunker-iW8V4klY.mjs";import{default as x}from"../packem_shared/defineRag-
|
|
1
|
+
import{default as o}from"../packem_shared/fixedWindowChunks-C461ahRE.mjs";import{markdownChunker as a,sentenceChunker as f,tokenChunker as n}from"../packem_shared/markdownChunker-iW8V4klY.mjs";import{default as x}from"../packem_shared/defineRag-CJudmmcj.mjs";import{contentHash as p,guessMimeTypeFromExtension as i}from"../packem_shared/contentHash-BIn6ECP8.mjs";import{hybridRank as d}from"../packem_shared/hybridRank-B4skyCLx.mjs";import{default as k}from"../packem_shared/bm25LexicalStore-DMUzAL0O.mjs";import{default as h}from"../packem_shared/matchesMetadataFilter-BbIOyA5g.mjs";import{batchReranker as g,scoreReranker as C}from"../packem_shared/batchReranker-Bc38FBLH.mjs";import{defineRagSource as E}from"../packem_shared/defineRagSource-Q3f3niU8.mjs";import{sqlLexicalStore as T}from"../packem_shared/sqlLexicalStore-4C_cIwef.mjs";import{sqliteVectorStore as y}from"../packem_shared/sqliteVectorStore-Dyv1x3eC.mjs";import{ragSyncTriggers as q}from"../packem_shared/ragSyncTriggers-D933fCwt.mjs";import{VECTORIZE_CAPABILITIES as A,vectorizeStore as F}from"../packem_shared/VECTORIZE_CAPABILITIES-l9ceRO5j.mjs";export{A as VECTORIZE_CAPABILITIES,g as batchReranker,k as bm25LexicalStore,p as contentHash,x as defineRag,E as defineRagSource,o as fixedWindowChunks,i as guessMimeTypeFromExtension,d as hybridRank,a as markdownChunker,h as matchesMetadataFilter,q as ragSyncTriggers,C as scoreReranker,f as sentenceChunker,T as sqlLexicalStore,y as sqliteVectorStore,n as tokenChunker,F as vectorizeStore};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/ai",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.128",
|
|
4
4
|
"description": "Workers AI helper for Lunora: provider-agnostic AI SDK access from functions, Workers AI by default",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -43,6 +43,10 @@
|
|
|
43
43
|
"types": "./dist/index.d.ts",
|
|
44
44
|
"import": "./dist/index.mjs"
|
|
45
45
|
},
|
|
46
|
+
"./create": {
|
|
47
|
+
"types": "./dist/create.d.ts",
|
|
48
|
+
"import": "./dist/create.mjs"
|
|
49
|
+
},
|
|
46
50
|
"./rag": {
|
|
47
51
|
"types": "./dist/rag/index.d.ts",
|
|
48
52
|
"import": "./dist/rag/index.mjs"
|
|
@@ -55,7 +59,9 @@
|
|
|
55
59
|
"dependencies": {
|
|
56
60
|
"@ai-sdk/anthropic": "4.0.71",
|
|
57
61
|
"@ai-sdk/openai": "4.0.83",
|
|
62
|
+
"@ai-sdk/provider-utils": "5.0.53",
|
|
58
63
|
"@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0",
|
|
64
|
+
"@lunora/wire": "1.0.0-alpha.1",
|
|
59
65
|
"ai": "7.0.127",
|
|
60
66
|
"workers-ai-provider": "4.0.0"
|
|
61
67
|
},
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const I={maxDimensions:1536,maxIdBytes:64,maxMetadataBytes:10240,maxTopK:100,maxTopKWithMetadata:50},y=(e,a)=>({capabilities:I,deleteByIds:(t,s)=>e.deleteByIds(a,t,s),getByIds:(t,s)=>e.getByIds(a,t,s),query:t=>e.query(a,t),upsert:t=>e.upsert(a,t)});export{I as VECTORIZE_CAPABILITIES,y as vectorizeStore};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as g}from"@lunora/errors";import{createWorkersAI as D}from"workers-ai-provider";import{buildAiGatewayMetadataFields as k,readEnv as y,AI_DEFAULT_MODEL_ENV as O,AI_DEFAULT_EMBEDDING_MODEL_ENV as P,AI_PROXY_URL_ENV as _,AI_PROXY_TOKEN_ENV as S,readAiGatewayEnvTags as H,AI_GATEWAY_ID_ENV as K,warnIgnoredBindingToken as Y}from"./AI_DEFAULT_EMBEDDING_MODEL_ENV-IwcHZiKj.mjs";import{wrapLanguageModel as q}from"ai";import{m as X,r as U}from"./usage-GPnTSqR8.mjs";import"./DEFAULT_MODEL_PRICES-CIJLs1Ah.mjs";const z={setAttribute:()=>{},setAttributes:()=>{}},G=async(e,t)=>t(G,z),Z=(e,{metrics:t,trace:a=G})=>{const r={"gen_ai.operation.name":"chat","gen_ai.request.model":e};return{wrapGenerate:async({doGenerate:n})=>a("ai.generate",async(h,u)=>{const i=await n();return U(e,i,u,t),i},r),wrapStream:async({doStream:n})=>{const h=(l,w)=>{const E=l.stream.getReader();let c={};return{...l,stream:new ReadableStream({cancel:async v=>{w({outcome:c}),await E.cancel(v)},pull:async v=>{let m;try{m=await E.read()}catch(b){w({error:b,failed:!0,outcome:c}),v.error(b);return}if(m.done){w({outcome:c}),v.close();return}m.value.type==="finish"&&(c={providerMetadata:m.value.providerMetadata,usage:m.value.usage}),v.enqueue(m.value)}})}},u=Promise.withResolvers(),i=Promise.withResolvers();return a("ai.stream",async(l,w)=>{u.resolve(h(await n(),i.resolve));const E=await i.promise;if(U(e,E.outcome,w,t),E.failed===!0)throw E.error},r).catch(u.reject),u.promise}}},x=(e,t,a)=>{let r=e;return t!==void 0&&typeof e!="string"&&(r=q({middleware:Z(a??X(e)??"unknown",t),model:e})),r};let T;const W=()=>(T??=Promise.all([import("@ai-sdk/openai"),import("workers-ai-provider/openai"),import("workers-ai-provider/anthropic")]).then(([e,{openai:t},{anthropic:a}])=>({createOpenAI:e.createOpenAI,plugins:[t,a]}),e=>{throw T=void 0,e}),T),$=(e,t,a)=>({doGenerate:async r=>a().then(n=>n.doGenerate(r)),doStream:async r=>a().then(n=>n.doStream(r)),modelId:e,provider:t,specificationVersion:"v4",get supportedUrls(){return a().then(r=>r.supportedUrls)}}),J=(e,t,a)=>({doEmbed:async r=>a().then(n=>n.doEmbed(r)),get maxEmbeddingsPerCall(){return a().then(r=>r.maxEmbeddingsPerCall)},modelId:e,provider:t,specificationVersion:"v4",get supportsParallelCalls(){return a().then(r=>r.supportsParallelCalls)}}),C=e=>!e.startsWith("@")&&e.includes("/"),I=e=>{throw new g("INTERNAL",`@lunora/ai: ${e} needs the \`AI\` binding (env.AI). In dev, Lunora leaves it out when there is no Cloudflare login: run \`wrangler login\` or set CLOUDFLARE_API_TOKEN. Elsewhere, add an \`ai\` binding to wrangler.jsonc. Or set ${_} to an OpenAI-compatible proxy for "<provider>/<model>" slugs.`)},B=()=>I("this model id");B.textEmbeddingModel=()=>I("this embedding model id");const Q=3040,ee=/^(?:[A-Z]\w*:\s*)?3040\s*:/iu,te=e=>e?.code===Q||e instanceof Error&&ee.test(e.message),re="default",ae=new Map([[401,"UNAUTHORIZED"],[403,"FORBIDDEN"],[404,"NOT_FOUND"],[408,"SERVICE_UNAVAILABLE"],[429,"RATE_LIMITED"]]),ne=e=>ae.get(e)??(e>=400&&e<500?"BAD_REQUEST":"SERVICE_UNAVAILABLE"),oe=300,ie=new Set(["127.0.0.1","[::1]","localhost"]),se=e=>{const t=y(e,_);if(t===void 0)return;const a=y(e,S),r=URL.canParse(t)?new URL(t):void 0;let n;if(r===void 0?n=`${_} is not a valid URL`:a!==void 0&&r.protocol!=="https:"&&!ie.has(r.hostname)&&(n=`${_} (${r.origin}) is not HTTPS, so ${S} would travel in cleartext — use an https:// URL`),n!==void 0){const i=()=>{throw new g("INTERNAL",`@lunora/ai: ${n}`)};return{chat:i,embedding:i}}let h;const u=()=>(h??=W().then(({createOpenAI:i})=>i({apiKey:a??"",baseURL:t,name:"lunora-proxy"})),h);return{chat:i=>$(i,"lunora-proxy",async()=>u().then(l=>l.chat(i))),embedding:i=>J(i,"lunora-proxy",async()=>u().then(l=>l.embedding(i)))}},de=(e,t)=>{const a=e===void 0?void 0:H(e);return a===void 0?t:{...t,tags:{...a,...t?.tags}}},ce=(e,t,a)=>{const r=k(a);if(e!==void 0)return r!==void 0&&e.metadata===void 0?{...e,metadata:r}:e;if(t===void 0)return;const n=y(t,K);if(n!==void 0)return Y(t),r===void 0?{id:n}:{id:n,metadata:r}},Ae=e=>{const{binding:t,defaultEmbeddingModel:a,defaultModel:r,env:n,gateway:h,metadata:u,provider:i,telemetry:l}=e,w=se(n),E=de(n,u),c=ce(h,n,E),v=h?.metadata===void 0?k(E):void 0,m=r??y(n,O),b=a??y(n,P),p=i??(t?D({binding:t,gateway:c}):B);let L;const M=()=>(L??=W().then(({plugins:o})=>D({binding:t,gateway:c,providers:o})),L);i===void 0&&t&&c!==void 0&&M().catch(()=>{});const V=(o,d)=>{if(!C(o))return d?.rejectIfBusy===void 0?p(o):p(o,{rejectIfBusy:d.rejectIfBusy});if(w!==void 0)return w.chat(o);const s=v===void 0?void 0:{metadata:v};return i!==void 0||!t?p(o,s):$(o,"workersai",async()=>M().then(A=>A(o,s)))},F=(o,d)=>{const s=o??m;if(s===void 0||s==="")throw new g("INTERNAL",`@lunora/ai: no model supplied and no default configured — pass a model id, or set ${O} in the Worker env (wrangler \`vars\` / \`.dev.vars\`)`);return typeof s=="string"?x(V(s,d),l,s):x(s,l)},j=o=>{if(w!==void 0&&C(o))return w.embedding(o);const d=p.textEmbeddingModel;if(typeof d!="function")throw new g("INTERNAL","@lunora/ai: the Workers AI provider does not expose `textEmbeddingModel`; pass an AI SDK EmbeddingModel (e.g. from @ai-sdk/openai) to embed()");return d.call(p,o)};return{embeddingModel:o=>{if(typeof o=="object")return o;const d=o??b;if(!d)throw new g("INTERNAL",`@lunora/ai: no embedding model supplied and no default configured — pass an embedding model id or an AI SDK EmbeddingModel, or set ${P} in the Worker env (wrangler \`vars\` / \`.dev.vars\`)`);return j(d)},model:F,run:async(o,d,s)=>{if(!t)return I("ai.run");const A=c!==void 0&&s?.gateway===void 0?{...s,gateway:c}:s;try{return await t.run(o,d,A)}catch(f){throw te(f)?new g("RATE_LIMITED",`@lunora/ai: Workers AI has no free capacity for ${o} (error 3040) — retry later`,{cause:f}):f}},websearch:async(o,d)=>{if(!t)return I("ai.websearch");if(typeof t.websearch!="function")throw new g("NOT_IMPLEMENTED","@lunora/ai: this Workers runtime's `AI` binding has no websearch() — update wrangler / @cloudflare/vite-plugin to a release that ships the Web Search API");const{gatewayId:s,...A}=d??{},f=await t.websearch({...A,gatewayId:s??c?.id??re,query:o});if(!f.ok){const R=(await f.text().catch(()=>"")).slice(0,oe);throw new g(ne(f.status),`@lunora/ai: web search failed with HTTP ${String(f.status)}${R===""?"":`: ${R}`}`)}const N=await f.json().catch(()=>{});if(!Array.isArray(N?.items))throw new g("INTERNAL","@lunora/ai: web search returned a body that is not JSON with an `items` array");return N},workersai:p}};export{Ae as default};
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import{LunoraError as f,isLunoraError as Me}from"@lunora/errors";import{tool as Ae,jsonSchema as Ne,embedMany as De,embed as Re}from"ai";import{s as Ce}from"./stable-key-B_BlboiY.mjs";import{r as ie,m as Q}from"./usage-GPnTSqR8.mjs";import Be from"./fixedWindowChunks-C461ahRE.mjs";import{c as $e,I as Oe}from"./concurrent-C6nqBv41.mjs";import{contentHash as Ke}from"./contentHash-BIn6ECP8.mjs";import{hybridRank as Ue}from"./hybridRank-B4skyCLx.mjs";import{VECTORIZE_CAPABILITIES as de,vectorizeStore as Qe}from"./VECTORIZE_CAPABILITIES-CUQDoxis.mjs";const Ve=1e3,Fe=200,Le=5,ze=4,ce=de.maxMetadataBytes===!1?Number.POSITIVE_INFINITY:de.maxMetadataBytes,Pe=2*1024,he="__ragChunk",pe="__ragSource",B="__ragText",j="__ragHash",Y="__ragChunks",O="__ragImportance",ge="__ragModel",je=new Set([he,Y,j,O,ge,pe,B]),Ye=(e,l,h,w)=>{if(w===!1)return;const v=new TextEncoder().encode(JSON.stringify(e)).length;if(v<=w)return;const K=(typeof e[B]=="string"?new TextEncoder().encode(e[B]).length:0)*2>v?"lower `chunkSize` (it counts characters, not bytes — multibyte text costs up to 3 bytes each), or supply `textStore` to move chunk text out of metadata entirely":"attach less per-source `metadata`";throw new f("BAD_REQUEST",`@lunora/ai/rag: chunk ${String(l)} of "${h}" carries ${String(v)} bytes of metadata, over the store's ${String(w)}-byte per-vector ceiling — ${K}`)},He=(e,l,h)=>{if(h===!1)return;const w=new TextEncoder().encode(e).length;if(!(w<=h))throw new f("BAD_REQUEST",`@lunora/ai/rag: chunk id "${e}" for source "${l}" is ${String(w)} bytes, over the store's ${String(h)}-byte per-vector id ceiling — shorten the source id (hash long keys before indexing them) or shorten the \`namespace\`, which is prefixed onto every chunk id`)},qe=/^[\w.-]{1,40}$/,fe=e=>e===void 0?"":`${encodeURIComponent(e)}#`,C=(e,l,h)=>`${fe(e)}${l}#${String(h)}`,ue=(e,l)=>{const h=fe(l),w=h!==""&&e.startsWith(h)?e.slice(h.length):e,v=w.lastIndexOf("#"),M=v===-1?Number.NaN:Number(w.slice(v+1));return v===-1||!Number.isInteger(M)||M<0?{chunkIndex:0,sourceId:w}:{chunkIndex:M,sourceId:w.slice(0,v)}},We=async e=>Ke(new TextEncoder().encode(e)),Ze=e=>{try{return Ce([e.text,e.metadata,e.importance])}catch{return}},Xe=(e,l)=>{const h=[],w=[];for(const v of e)v.score>=l?h.push(v):w.push(v.id);return{kept:h,rejectedIds:w}},Je=e=>e!==void 0&&Object.keys(e).length>0,P=e=>{if(!e)return;const l=Object.entries(e).filter(([h])=>!je.has(h));return l.length>0?Object.fromEntries(l):void 0},le=new Set,Ge=e=>{le.has(e)||(le.add(e),console.warn(`[@lunora/ai/rag] index "${e}" is used without a namespace — in a multi-tenant/sharded
|
|
2
|
-
app this shares one tenant's chunks (text included) with every other tenant, since
|
|
3
|
-
Vectorize indexes are account-global. Pass \`namespace\` (the shard/tenant key) on both
|
|
4
|
-
index() and retrieve(). Single-tenant apps suppress this via { allowSharedNamespace: true }.`))},et=e=>e.map(l=>`[source:${l.sourceId}#${String(l.chunkIndex)}]
|
|
5
|
-
${l.text}`).join(`
|
|
6
|
-
|
|
7
|
-
`),me=(e,l)=>{if(typeof e=="object")return e;if(l===void 0)throw new f("INTERNAL","@lunora/ai/rag: `embeddingModel` is a Workers AI model id (or omitted) but the bound context has no `ai` (env.AI). Pass an AI SDK EmbeddingModel object to embed without Workers AI, or bind a context whose `ctx.ai` is wired.");return l.embeddingModel(e)},tt=e=>{if(e===void 0)throw new f("INTERNAL","@lunora/ai/rag: the bound context has no `vectors` (env.VECTORIZE) and no `store` is configured — bind a context whose `ctx.vectors` is wired, or configure `store` (e.g. `sqliteVectorStore`) to back this index without Vectorize.");return e},mt=e=>{if(typeof e.index!="string"||e.index.length===0)throw new f("BAD_REQUEST","@lunora/ai/rag: `index` must be a non-empty Vectorize index name");const l=e.chunkSize??Ve,h=e.chunkOverlap??Fe;if(!Number.isInteger(l)||l<1)throw new f("BAD_REQUEST","@lunora/ai/rag: `chunkSize` must be a positive integer");if(!Number.isInteger(h)||h<0||h>=l)throw new f("BAD_REQUEST","@lunora/ai/rag: `chunkOverlap` must be a non-negative integer smaller than `chunkSize`");const w=ce-Pe;if(!e.chunk&&!e.textStore&&!e.store&&l>w)throw new f("BAD_REQUEST",`@lunora/ai/rag: \`chunkSize\` of ${String(l)} leaves no room under Vectorize's ${String(ce)}-byte metadata limit, which also carries this chunk's bookkeeping and any \`metadata\` you attach — keep it under ${String(w)}, or supply \`textStore\` to move chunk text out of metadata entirely`);const v=e.topK??Le;if(!Number.isInteger(v)||v<1)throw new f("BAD_REQUEST","@lunora/ai/rag: `topK` must be a positive integer");if(e.maxEmbeddingDimensions!==void 0&&e.maxEmbeddingDimensions!==!1&&(!Number.isInteger(e.maxEmbeddingDimensions)||e.maxEmbeddingDimensions<1))throw new f("BAD_REQUEST","@lunora/ai/rag: `maxEmbeddingDimensions` must be a positive integer, or `false` to disable the check");if(e.embeddingModelVersion!==void 0&&!qe.test(e.embeddingModelVersion))throw new f("BAD_REQUEST",'@lunora/ai/rag: `embeddingModelVersion` must match /^[A-Za-z0-9._-]{1,40}$/ (a short, stable tag like "bge-v1.5")');if(e.candidates!==void 0&&(!Number.isInteger(e.candidates)||e.candidates<1))throw new f("BAD_REQUEST","@lunora/ai/rag: `candidates` must be a positive integer");if(e.cacheEmbeddings!==void 0&&(!Number.isInteger(e.cacheEmbeddings)||e.cacheEmbeddings<0))throw new f("BAD_REQUEST","@lunora/ai/rag: `cacheEmbeddings` must be a non-negative integer");const M=e.cacheEmbeddings??0,K=e.rerank,we=e.chunk??(p=>Be(p,l,h)),{textStore:k}=e,$=e.embeddingModelVersion,V=p=>$===void 0?p:p===void 0?$:`${$}::${p}`;return p=>{const E=e.store?e.store(p):Qe(tt(p.vectors),e.index),H=k?E.capabilities.maxTopK:E.capabilities.maxTopKWithMetadata,U=e.maxEmbeddingDimensions??E.capabilities.maxDimensions;let N;const q=typeof p.trace=="function"?p.trace:void 0,W=typeof p.metrics?.count=="function"?p.metrics:void 0;let Z=U===!1;const X=(t,r)=>{if(Z||(Z=!0,U===!1||t<=U))return;const n=Q(r);throw new f("BAD_REQUEST",`@lunora/ai/rag: embedding model${n===void 0?"":` "${n}"`} produces ${String(t)}-dimension vectors, over the ${String(U)}-dimension ceiling of index "${e.index}" — either truncate them with the provider's \`dimensions\` option (Matryoshka models such as text-embedding-3-large support this), or set \`maxEmbeddingDimensions: false\` if this index is not Vectorize-backed`)},D=new Map,be=(t,r)=>{if(M!==0)for(D.set(t,r);D.size>M;){const n=D.keys().next();if(n.done===!0)break;D.delete(n.value)}},J=async t=>{const r=D.get(t);if(r!==void 0)return r;N??=me(e.embeddingModel,p.ai);const n=N,o=async a=>{const{embedding:c,providerMetadata:b,usage:d}=await Re({model:n,value:t});return X(c.length,n),ie(Q(n),{providerMetadata:b,usage:{inputTokens:{total:d.tokens}}},a,W),be(t,c),c};if(q===void 0)return o();const s=Q(n),i=typeof p.conversationId=="string"&&p.conversationId.length>0?p.conversationId:void 0;return q("ai.embed",(a,c)=>o(c),{"gen_ai.operation.name":"embeddings",...s===void 0?{}:{"gen_ai.request.model":s},...i===void 0?{}:{"gen_ai.conversation.id":i}})},ve=async(t,r,n)=>{if(!e.transformQuery||r?.transformQuery===!1)return[t];const o=typeof p.conversationId=="string"&&p.conversationId.length>0?p.conversationId:void 0,s=await e.transformQuery(t,{conversationId:o,namespace:n}),i=(typeof s=="string"?[s]:[...s]).map(a=>a.trim()).filter(a=>a.length>0);return i.length>0?i:[t]},ye=async t=>{const r=new Map,n=[...new Set(t.filter(o=>!D.has(o)))];if(n.length<2)return r;N??=me(e.embeddingModel,p.ai);try{const{embeddings:o,providerMetadata:s,usage:i}=await De({model:N,values:n});if(ie(Q(N),{providerMetadata:s,usage:{inputTokens:{total:i.tokens}}},void 0,W),o.length!==n.length)return r;const[a]=o;a!==void 0&&X(a.length,N);for(const[c,b]of n.entries())r.set(b,o[c])}catch(o){if(Me(o))throw o}return r},F=t=>{if(t===void 0){if(e.requireNamespace)throw new f("BAD_REQUEST",`@lunora/ai/rag: index "${e.index}" requires a namespace (requireNamespace is set) — pass the tenant/shard key on index()/retrieve()/remove()`);e.allowSharedNamespace||Ge(e.index)}},G=async(t,r)=>{const[n]=await E.getByIds([C(r,t,0)],r),o=n?.metadata?.[j],s=n?.metadata?.[Y];return{chunks:typeof s=="number"&&Number.isInteger(s)&&s>0?s:void 0,hash:typeof o=="string"?o:void 0}},ee=async(t,r,n,o)=>{const s=Array.from({length:n-r},(i,a)=>C(o,t,r+a));s.length!==0&&(await E.deleteByIds(s,o),await k?.remove?.(s,{namespace:o}),await e.lexicalStore?.remove?.(s,{namespace:o}))},Ee=async t=>{if(F(t.namespace),t.importance!==void 0&&(typeof t.importance!="number"||t.importance<0||t.importance>1))throw new f("BAD_REQUEST","@lunora/ai/rag: `importance` must be a number in [0, 1]");const r=V(t.namespace),n=Ze(t),o=await We(n??t.text),s=await G(t.id,r);if(t.reindex!==!0&&n!==void 0&&s.hash===o&&s.chunks!==void 0)return{chunks:s.chunks,ids:Array.from({length:s.chunks},(m,u)=>C(r,t.id,u)),unchanged:!0};const i=we(t.text),a=i.map((m,u)=>C(r,t.id,u)),c=a.at(-1);if(c!==void 0&&He(c,t.id,E.capabilities.maxIdBytes),i.length===0&&t.allowEmptySources===!1)throw new f("BAD_REQUEST",`@lunora/ai/rag: source "${t.id}" produced zero chunks — set allowEmptySources: true to allow this`);if(i.length>0){const m=i.map((u,y)=>({chunkIndex:y,id:a[y],sourceId:t.id,text:u,...t.metadata===void 0?{}:{metadata:t.metadata}}));k&&await k.put(m,{namespace:r}),e.lexicalStore&&await e.lexicalStore.index(m,{namespace:r})}const b=await ye(i),d=async m=>b.get(m)??await J(m);return await $e(i,Oe,async(m,u)=>{const y=a[u],x={...t.metadata,[he]:u,[pe]:t.id};k||(x[B]=m),t.importance!==void 0&&(x[O]=t.importance),u===0&&(x[j]=o,x[Y]=i.length,$!==void 0&&(x[ge]=$)),Ye(x,u,t.id,E.capabilities.maxMetadataBytes),await E.upsert({embed:d,id:y,input:m,metadata:x,namespace:r}),t.onChunk?.({chunkIndex:u,id:y,text:m,total:i.length})}),s.chunks!==void 0&&s.chunks>i.length&&await ee(t.id,i.length,s.chunks,r),{chunks:i.length,ids:a,unchanged:!1}},xe=async t=>{F(t.namespace);const r=V(t.namespace),o=(await G(t.id,r)).chunks??1;await ee(t.id,0,o,r)},te=async(t,r)=>{const n=new Map;if(t.length===0)return n;if(k){const s=await k.getMany(t,{namespace:r});for(const[i,a]of t.entries()){const c=s[i];typeof c=="string"&&n.set(a,c)}return n}const o=await E.getByIds(t,r);for(const s of o){const i=s.metadata?.[B];typeof i=="string"&&n.set(s.id,i)}return n},Ie=async(t,r,n)=>{const o=r?.chunkContext?.before??0,s=r?.chunkContext?.after??0;if(o===0&&s===0)return t;if(!Number.isInteger(o)||o<0||!Number.isInteger(s)||s<0)throw new f("BAD_REQUEST","@lunora/ai/rag: `chunkContext.before`/`chunkContext.after` must be non-negative integers");const i=new Map(t.map(d=>[d.id,d.text])),a=new Set;for(const d of t)for(let m=-o;m<=s;m+=1){const u=d.chunkIndex+m,y=C(n,d.sourceId,u);m!==0&&u>=0&&!i.has(y)&&a.add(y)}const c=await te([...a],n),b=(d,m)=>{const u=C(n,d,m);return i.get(u)??c.get(u)};return t.map(d=>{const m=[];for(let u=-o;u<=s;u+=1){const y=u===0?d.text:b(d.sourceId,d.chunkIndex+u);y!==void 0&&m.push(y)}return{...d,text:m.join(`
|
|
8
|
-
`)}})},Se=t=>{if(typeof t=="string"){const r=e.filters!==void 0&&Object.hasOwn(e.filters,t)?e.filters[t]:void 0;if(!r)throw new f("NOT_FOUND",`@lunora/ai/rag: unknown named filter "${t}" — must be one of the keys declared in RagConfig.filters`);return r.filter}return t},ke=(t,r)=>t.matches.map(n=>{const o=n.metadata??{},s=ue(n.id,r),i=o[B],a=o[O],c=typeof a=="number"&&a>=0&&a<=1?a:1;return{chunkIndex:s.chunkIndex,id:n.id,importance:c,metadata:P(o),score:n.score*c,sourceId:s.sourceId,text:typeof i=="string"?i:""}}),_e=async(t,r)=>{if(!k)return t;const n=t.map(a=>a.id),[o,s]=await Promise.all([te(n,r),E.getByIds(n,r)]),i=new Map(s.map(a=>[a.id,a.metadata]));return t.flatMap(a=>{const c=o.get(a.id);if(c===void 0)return[];const b=i.get(a.id),d=b?.[O],m=typeof d=="number"&&d>=0&&d<=1?d:a.importance,y=(a.importance===0?0:a.score/a.importance)*m;return[{...a,importance:m,metadata:P(b)??a.metadata,score:y,text:c}]})},re=async(t,r,n)=>{const o=t.map(a=>a.id).filter(a=>!r.has(a)),s=o.length===0?[]:await E.getByIds(o,n),i=new Map(s.map(a=>[a.id,a.metadata]));return t.map(a=>{const c=ue(a.id,n),b=i.get(a.id),d=b?.[O];return{chunkIndex:c.chunkIndex,id:a.id,importance:typeof d=="number"&&d>=0&&d<=1?d:1,metadata:P(b),score:a.score,sourceId:c.sourceId,text:a.text}})},ne=async(t,r)=>{F(r?.namespace);const n=V(r?.namespace),o=Se(r?.filter),s=e.rlsFilter?await e.rlsFilter(p.auth):void 0,i=s?{...o,...s}:o,a=Math.min(r?.topK??v,H),c=await ve(t,r,n),b=c[0],d=K!==void 0&&r?.rerank!==!1,u=d||e.lexicalStore!==void 0||e.graphStore!==void 0||c.length>1?Math.min(e.candidates??a*ze,H):a,y=r?.minScore,x=new Set,ae=async g=>{const _=await E.query({embed:J,filter:i,input:g,namespace:n,returnMetadata:k?"indexed":"all",topK:u}),A=await _e(ke(_,n),n);if(y===void 0)return A;const{kept:S,rejectedIds:T}=Xe(A,y);for(const Te of T)x.add(Te);return S},R=[{chunks:await ae(b)}];for(const g of c.slice(1))R.push({chunks:await ae(g)});if(e.lexicalStore){const g=await e.lexicalStore.search(b,{filter:i,namespace:n,topK:e.lexicalTopK??u}),_=new Set(R.flatMap(S=>S.chunks).map(S=>S.id)),A=g.filter(S=>!x.has(S.id)||_.has(S.id));R.push({chunks:await re(A,_,n)})}const L=R.flatMap(g=>[...g.chunks]);if(e.graphStore&&L.length>0&&(e.graphStore.enforcesFilter||!Je(i))){const g=[...new Set(L.map(T=>T.sourceId))],_=await e.graphStore.related(g,{filter:i,namespace:n,topK:e.graphTopK??u}),A=new Set(L.map(T=>T.id)),S=_.filter(T=>!x.has(T.id)||A.has(T.id));R.push({chunks:await re(S,A,n),weight:"proximity"})}const z=R.filter(g=>g.chunks.length>0);let I=z.length>1?[...Ue(z)]:[...z[0]?.chunks??[]];I.sort((g,_)=>_.score-g.score),d&&(I=[...await K(b,I)]),I=I.slice(0,a),I=[...await Ie(I,r,n)];const se=[],oe=new Set;for(const g of I)oe.has(g.sourceId)||(oe.add(g.sourceId),se.push({id:g.sourceId,metadata:g.metadata,weight:g.importance}));return r?.onRetrieve?.({matches:I.length,query:t}),{chunks:I,context:et(I),sources:se}};return{asTool:t=>Ae({description:t?.description??`Search the "${e.index}" knowledge base for passages relevant to a natural-language query.`,execute:async({query:r})=>ne(r,{namespace:t?.namespace,topK:t?.topK}),inputSchema:Ne({properties:{query:{description:"The natural-language search query.",type:"string"}},required:["query"],type:"object"})}),index:Ee,remove:xe,retrieve:ne}}};export{mt as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as l}from"./stable-key-B_BlboiY.mjs";const v=c=>{const m=c.delayMs??0,y=(d,a)=>c.id===void 0?a:c.id(d),e=d=>d===void 0?void 0:c.text(d),u=d=>c.metadata?.(d),p=d=>c.namespace?.(d),o=(d,a)=>{const t=p(d);return{id:y(d,a),...t===void 0?{}:{namespace:t}}},f=(d,a,t)=>{const s=u(d);return{...o(d,a),...s===void 0?{}:{metadata:s},text:t}},g=(d,a)=>{try{return l(u(d))===l(u(a))}catch{return!1}},r=async(d,a)=>{await d.scheduler.runAfter(m,c.action,a)};return{afterDelete:async(d,a)=>{const t=a.previous??a.doc;await r(d,{deleted:!0,...t===void 0?{id:a.id}:o(t,a.id)})},afterInsert:async(d,a)=>{const t=e(a.doc);t===void 0||a.doc===void 0||await r(d,f(a.doc,a.id,t))},afterUpdate:async(d,a)=>{if(a.doc===void 0)return;const t=e(a.doc),s=e(a.previous),i=o(a.doc,a.id),n=a.previous===void 0?i:o(a.previous,a.id),w=n.id!==i.id||n.namespace!==i.namespace,h=c.metadata!==void 0&&a.previous!==void 0&&!g(a.doc,a.previous);if(w)await r(d,{deleted:!0,...n});else if(t===s&&!h)return;await(t===void 0?r(d,{deleted:!0,...i}):r(d,f(a.doc,a.id,t)))}}};export{v as ragSyncTriggers};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import x from"./matchesMetadataFilter-BbIOyA5g.mjs";import{p as I,a as q,r as y,c as F,i as O}from"./sql-D5aqEMCY.mjs";import{LunoraError as _}from"@lunora/errors";const p=500,C=4096,R=(d,r)=>JSON.stringify([d,r]),D=d=>{const{dimensions:r,exec:o,table:m}=d,s=`${m}_ann`,E=`${m}_ann_ready`,u=(a,t)=>{if(a!==r)throw new _("RAG_DIMENSION_MISMATCH",`@lunora/ai/rag: ${t} is ${String(a)}-dimension but the \`ann\` index is ${String(r)}-dimension — set \`ann.dimensions\` to the embedding model's width, or reindex into a new \`table\` after changing models`)},g=async(a,t,i)=>{await o(`INSERT INTO ${s} (ref, namespace, embedding, id) VALUES (?, ?, ?, ?)`,[R(a,t),a,i,t])},S=async()=>{let a=0;for(;;){const t=await o(`SELECT rowid, id, namespace, vector FROM ${m} WHERE rowid > ? ORDER BY rowid LIMIT ?`,[a,p]);for(const i of t)u(JSON.parse(String(i.vector)).length,`stored vector "${String(i.id)}"`),await g(String(i.namespace),String(i.id),String(i.vector));if(t.length<p)return;a=Number(t.at(-1)?.rowid)}};return{ensure:async()=>{if((await o("SELECT name FROM sqlite_master WHERE name = ?",[E])).length>0){const[t]=await o(`SELECT dimensions FROM ${E}`,[]);u(Number(t?.dimensions),"the existing index");return}await o(`DROP TABLE IF EXISTS ${s}`,[]);try{await o(`CREATE VIRTUAL TABLE ${s} USING vec0(ref TEXT PRIMARY KEY, namespace TEXT PARTITION KEY, embedding FLOAT[${String(r)}] distance_metric=cosine, +id TEXT)`,[])}catch(t){throw t instanceof Error&&t.message.includes("no such module: vec0")?new Error("@lunora/ai/rag: sqliteVectorStore `ann` needs the sqlite-vec extension in this SQLite (`no such module: vec0`) — on celld set the `sqlite_vec` compatibility flag, on node:sqlite load the extension",{cause:t}):t}await S(),await o(`CREATE TABLE ${E} (dimensions INTEGER NOT NULL)`,[]),await o(`INSERT INTO ${E} (dimensions) VALUES (?)`,[r])},nearest:async(a,t,i)=>(u(t.length,"the query embedding"),(await o(`SELECT id, distance FROM ${s} WHERE embedding MATCH ? AND k = ? AND namespace = ?`,[JSON.stringify([...t]),Math.min(i,C),a])).map(e=>({distance:Number(e.distance),id:String(e.id)}))),put:async(a,t,i)=>{u(i.length,`the embedding for "${t}"`),await o(`DELETE FROM ${s} WHERE ref = ?`,[R(a,t)]),await g(a,t,JSON.stringify([...i]))},remove:async(a,t)=>{await o(`DELETE FROM ${s} WHERE ref IN (${I(t.length)})`,t.map(i=>R(a,i)))}}},B="lunora_rag_vectors",U=5e4,$=100,V=null,h=d=>d??"",W=d=>{if(typeof d.exec!="function")throw new TypeError("@lunora/ai/rag: sqliteVectorStore requires an `exec` function");const r=q(d.table??B,"sqliteVectorStore `table`"),o=d.maxScan??U,{ann:m,exec:s}=d;if(m!==void 0&&(!Number.isInteger(m.dimensions)||m.dimensions<=0))throw new TypeError("@lunora/ai/rag: sqliteVectorStore `ann.dimensions` must be a positive integer");const E=m===void 0?void 0:D({dimensions:m.dimensions,exec:s,table:r}),u={maxDimensions:d.maxDimensions??m?.dimensions??!1,maxIdBytes:!1,maxMetadataBytes:!1,maxTopK:$,maxTopKWithMetadata:$};let g;const S=async()=>{g??=(async()=>{await s(`CREATE TABLE IF NOT EXISTS ${r} (id TEXT NOT NULL, namespace TEXT NOT NULL DEFAULT '', vector TEXT NOT NULL, metadata TEXT, PRIMARY KEY (namespace, id))`,[]),await s(`CREATE INDEX IF NOT EXISTS ${r}_namespace ON ${r} (namespace)`,[]),await E?.ensure()})().catch(e=>{throw g=void 0,e}),await g},b=async e=>{if(await S(),!e.embed)throw new TypeError("@lunora/ai/rag: sqliteVectorStore requires an `embed` function on upsert");const c=await e.embed(e.input),n=h(e.namespace);await E?.put(n,e.id,c),await s(`INSERT INTO ${r} (id, namespace, vector, metadata) VALUES (${I(4)}) ON CONFLICT(namespace, id) DO UPDATE SET vector = excluded.vector, metadata = excluded.metadata`,[e.id,n,JSON.stringify([...c]),e.metadata===void 0?V:JSON.stringify(e.metadata)])},a=async(e,c)=>{if(await S(),e.length===0)return[];const n=[];for(const w of O(e)){const f=await s(`SELECT id, metadata FROM ${r} WHERE namespace = ? AND id IN (${I(w.length)})`,[h(c),...w]);for(const l of f){const T=y(l.metadata);n.push({id:String(l.id),...T===void 0?{}:{metadata:T}})}}return n},t=async(e,c,n)=>{const w=h(n.namespace),f=await e.nearest(w,c,n.topK??10),l=await a(f.map(L=>L.id),w),T=new Map(l.map(L=>[L.id,L])),N=[];for(const{distance:L,id:A}of f){const v=T.get(A);v!==void 0&&N.push({id:A,score:1-L,...n.returnMetadata==="none"||v.metadata===void 0?{}:{metadata:v.metadata}})}return{count:N.length,matches:N}};return{capabilities:u,deleteByIds:async(e,c)=>{if(await S(),e.length!==0)for(const n of O(e))await s(`DELETE FROM ${r} WHERE namespace = ? AND id IN (${I(n.length)})`,[h(c),...n]),await E?.remove(h(c),n)},getByIds:a,query:async e=>{await S();let c;if(e.embed&&e.input!==void 0)c=await e.embed(e.input);else throw new TypeError("@lunora/ai/rag: sqliteVectorStore query requires both `input` and `embed`");if(E!==void 0&&e.filter===void 0)return t(E,c,e);const n=await s(`SELECT id, vector, metadata FROM ${r} WHERE namespace = ? LIMIT ?`,[h(e.namespace),o+1]);if(n.length>o)throw new RangeError(`@lunora/ai/rag: sqliteVectorStore scanned ${String(n.length)} vectors in namespace "${h(e.namespace)}", over the ${String(o)} limit — search here is brute force and linear, so this namespace has outgrown it. Shard it further, or move this index to Vectorize or a pgvector backend`);const w=[];for(const l of n){const T=y(l.metadata);if(!x(T,e.filter))continue;const N=y(l.vector);N!==void 0&&w.push({id:String(l.id),score:F(c,N),...e.returnMetadata==="none"||T===void 0?{}:{metadata:T}})}const f=w.toSorted((l,T)=>T.score-l.score).slice(0,e.topK??10);return{count:f.length,matches:f}},upsert:b}};export{W as sqliteVectorStore};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const a=/["\\\u0000-\u001F\uD800-\uDFFF]/,f=t=>a.test(t)?JSON.stringify(t):`"${t}"`,y=t=>{if(t===void 0)return"null";if(typeof t=="bigint")throw new TypeError("stableStringify: cannot use a bigint in a stable JSON cache key — pass it as a string, or use stableWireKey");if(typeof t=="number"){if(Number.isNaN(t))return"nan";if(t===1/0)return"inf";if(t===-1/0)return"-inf";if(Object.is(t,-0))return"-0"}if(typeof t=="string")return f(t);if(t===null||typeof t!="object")return JSON.stringify(t);if(Array.isArray(t)){let r="[";for(let n=0;n<t.length;n++)n>0&&(r+=","),r+=y(t[n]);return r+"]"}const i=Object.getPrototypeOf(t);if(i!==null&&i!==Object.prototype){const r=t.constructor?.name??"value";throw new TypeError(`stableStringify: cannot use a ${r} in a stable JSON cache key — only plain objects, arrays, and JSON primitives are supported (wire-typed values key via stableWireKey)`)}const o=t,c=Object.keys(o).sort();let e="{",s=!0;for(const r of c){const n=o[r];n!==void 0&&(s?s=!1:e+=",",e+=f(r),e+=":",e+=y(n))}return e+"}"};export{y as s};
|