@ultimat3/ai 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/evals.ts ADDED
@@ -0,0 +1,242 @@
1
+ // Evals are a test type.
2
+ //
3
+ // Not a notebook, not a dashboard, not a weekly report — a `bun test` case that fails CI.
4
+ // A prompt change that drops a score below its recorded baseline should break the build exactly
5
+ // like a type error does, because the consequence is the same: shipping something wrong.
6
+ //
7
+ // The gate is the DROP, never the absolute score: an absolute floor fails every eval at once the
8
+ // day the provider ships a slightly different model, which teaches everyone to lower thresholds
9
+ // until they measure nothing. `tolerance` is how far a score may fall before it is a regression.
10
+ //
11
+ // Every result is filed against a prompt's content hash, so a score is always attributable
12
+ // to an exact prompt rather than "whatever was in main that day".
13
+
14
+ import { EvalBaselineMissingError, EvalThresholdError } from './errors';
15
+ import type { EvalBaseline, Regression } from './eval-baseline';
16
+ import {
17
+ baselinePath,
18
+ describeRegression,
19
+ readBaseline,
20
+ recordingBaselines,
21
+ regressionsAgainst,
22
+ writeBaseline,
23
+ } from './eval-baseline';
24
+ import type { Gateway } from './gateway';
25
+ import type { Prompt, PromptVars } from './prompt';
26
+ import { describePrompts } from './prompt';
27
+ import type { Scorer } from './scorers';
28
+ import { clampScore } from './scorers';
29
+
30
+ /** One case: the variables to render with, plus what a good answer looks like. */
31
+ export interface EvalCase<V extends PromptVars = PromptVars> {
32
+ readonly name: string;
33
+ readonly vars: V;
34
+ /** Reference answer, or the substring/JSON the scorers check for. */
35
+ readonly expected?: string;
36
+ }
37
+
38
+ export interface DefineEvalInput<V extends PromptVars = PromptVars> {
39
+ readonly name: string;
40
+ readonly prompt: Prompt<V>;
41
+ readonly cases: readonly EvalCase<V>[];
42
+ readonly scorers: readonly Scorer[];
43
+ /**
44
+ * The committed scores this run is compared against. Write it as
45
+ * `import.meta.resolve('./name.baseline.json')` — a cwd-relative path resolves to a different
46
+ * file depending on where the suite was started.
47
+ */
48
+ readonly baseline: string;
49
+ /** How far a score may fall before it is a regression. Explicit: widening it is a diff. */
50
+ readonly tolerance: number;
51
+ readonly maxTokens?: number;
52
+ }
53
+
54
+ export interface CaseResult {
55
+ readonly case: string;
56
+ readonly output: string;
57
+ readonly score: number;
58
+ readonly perScorer: Readonly<Record<string, number>>;
59
+ }
60
+
61
+ export interface EvalResult {
62
+ readonly name: string;
63
+ readonly promptRef: string;
64
+ /** The prompt content hash. A score means nothing without it. */
65
+ readonly promptHash: string;
66
+ readonly score: number;
67
+ /** The recorded run mean, or `undefined` when this eval has never been recorded. */
68
+ readonly baseline: number | undefined;
69
+ readonly tolerance: number;
70
+ /** Every score that fell further than `tolerance` — the run mean and each case. */
71
+ readonly regressions: readonly Regression[];
72
+ /** A baseline exists and nothing regressed. Anything else is a red gate. */
73
+ readonly passed: boolean;
74
+ readonly cases: readonly CaseResult[];
75
+ }
76
+
77
+ export interface Eval<V extends PromptVars = PromptVars> {
78
+ readonly name: string;
79
+ readonly prompt: Prompt<V>;
80
+ readonly tolerance: number;
81
+ /** The baseline file, as declared — what `x verify` reports when it cannot be read. */
82
+ readonly baseline: string;
83
+ /** Score every case against the baseline. Never throws on a regression — `assert` does that. */
84
+ run(gateway: Gateway): Promise<EvalResult>;
85
+ /**
86
+ * Run and throw `X_EVAL_THRESHOLD` on a regression. This is the line a test file calls, so a
87
+ * prompt edit that broke a case is a red test naming that case and both its scores.
88
+ *
89
+ * Under `ULTIMATE_EVAL_RECORD=1` it writes the baseline instead of gating on it.
90
+ */
91
+ assert(gateway: Gateway): Promise<EvalResult>;
92
+ }
93
+
94
+ /** What `x verify` reads to decide whether every prompt is evaluated. */
95
+ export interface EvalFact {
96
+ readonly name: string;
97
+ readonly prompt: string;
98
+ readonly promptId: string;
99
+ readonly tolerance: number;
100
+ readonly baseline: string;
101
+ }
102
+
103
+ const registry = new Map<string, Eval>();
104
+
105
+ export function defineEval<V extends PromptVars>(input: DefineEvalInput<V>): Eval<V> {
106
+ const evaluation: Eval<V> = {
107
+ name: input.name,
108
+ prompt: input.prompt,
109
+ tolerance: input.tolerance,
110
+ baseline: input.baseline,
111
+ run: (gateway) => runEval(input, gateway),
112
+ assert: (gateway) => assertEval(input, gateway),
113
+ };
114
+ registry.set(input.name, evaluation as Eval);
115
+ return evaluation;
116
+ }
117
+
118
+ export function describeEvals(): readonly EvalFact[] {
119
+ return [...registry.values()]
120
+ .map((evaluation) => ({
121
+ name: evaluation.name,
122
+ prompt: evaluation.prompt.ref,
123
+ promptId: evaluation.prompt.id,
124
+ tolerance: evaluation.tolerance,
125
+ baseline: evaluation.baseline,
126
+ }))
127
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
128
+ }
129
+
130
+ /**
131
+ * Every registered prompt no eval names, by ID rather than by version: old versions are retained
132
+ * so traces stay interpretable, and an eval on the current version evaluates that lineage.
133
+ */
134
+ export function promptsWithoutEvals(): readonly Prompt[] {
135
+ const covered = new Set([...registry.values()].map((evaluation) => evaluation.prompt.id));
136
+ return describePrompts().filter((prompt) => !covered.has(prompt.id));
137
+ }
138
+
139
+ export function resetEvals(): void {
140
+ registry.clear();
141
+ }
142
+
143
+ async function assertEval<V extends PromptVars>(
144
+ input: DefineEvalInput<V>,
145
+ gateway: Gateway,
146
+ ): Promise<EvalResult> {
147
+ const path = baselinePath(input.baseline, input.name);
148
+ const result = await runEval(input, gateway);
149
+
150
+ if (recordingBaselines()) {
151
+ await writeBaseline(path, baselineFrom(result));
152
+ return { ...result, baseline: result.score, regressions: [], passed: true };
153
+ }
154
+ if (result.baseline === undefined) {
155
+ throw new EvalBaselineMissingError({
156
+ eval: input.name,
157
+ path,
158
+ reason: 'has never been recorded',
159
+ });
160
+ }
161
+ if (result.regressions.length > 0) {
162
+ throw new EvalThresholdError({
163
+ eval: result.name,
164
+ score: result.score,
165
+ baseline: result.baseline,
166
+ tolerance: result.tolerance,
167
+ promptVersion: `${result.promptRef} (${result.promptHash})`,
168
+ regressed: result.regressions.map(describeRegression),
169
+ });
170
+ }
171
+ return result;
172
+ }
173
+
174
+ async function runEval<V extends PromptVars>(
175
+ input: DefineEvalInput<V>,
176
+ gateway: Gateway,
177
+ ): Promise<EvalResult> {
178
+ const cases: CaseResult[] = [];
179
+ for (const testCase of input.cases) {
180
+ const generated = await gateway.generate({
181
+ messages: [{ role: 'user' as const, content: input.prompt.render(testCase.vars) }],
182
+ maxTokens: input.maxTokens ?? 1_024,
183
+ ...(input.prompt.system !== undefined ? { system: input.prompt.system } : {}),
184
+ ...(input.prompt.model !== undefined ? { model: input.prompt.model } : {}),
185
+ ...(input.prompt.effort !== undefined ? { effort: input.prompt.effort } : {}),
186
+ });
187
+ const perScorer: Record<string, number> = {};
188
+ for (const scorer of input.scorers) {
189
+ perScorer[scorer.name] = clampScore(
190
+ await scorer.score({
191
+ output: generated.text,
192
+ ...(testCase.expected !== undefined ? { expected: testCase.expected } : {}),
193
+ }),
194
+ );
195
+ }
196
+ cases.push({
197
+ case: testCase.name,
198
+ output: generated.text,
199
+ score: mean(Object.values(perScorer)),
200
+ perScorer,
201
+ });
202
+ }
203
+
204
+ const score = mean(cases.map((c) => c.score));
205
+ const recorded = await readBaseline(baselinePath(input.baseline, input.name));
206
+ const regressions =
207
+ recorded === undefined
208
+ ? []
209
+ : regressionsAgainst({
210
+ baseline: recorded,
211
+ score,
212
+ cases: caseScores(cases),
213
+ tolerance: input.tolerance,
214
+ });
215
+
216
+ return {
217
+ name: input.name,
218
+ promptRef: input.prompt.ref,
219
+ promptHash: input.prompt.hash,
220
+ score,
221
+ baseline: recorded?.score,
222
+ tolerance: input.tolerance,
223
+ regressions,
224
+ passed: recorded !== undefined && regressions.length === 0,
225
+ cases,
226
+ };
227
+ }
228
+
229
+ const caseScores = (cases: readonly CaseResult[]): Record<string, number> =>
230
+ Object.fromEntries(cases.map((c) => [c.case, c.score]));
231
+
232
+ /** What a run records: the numbers, and the exact prompt that produced them. */
233
+ export const baselineFrom = (result: EvalResult): EvalBaseline => ({
234
+ eval: result.name,
235
+ prompt: result.promptRef,
236
+ promptHash: result.promptHash,
237
+ score: result.score,
238
+ cases: caseScores(result.cases),
239
+ });
240
+
241
+ const mean = (values: readonly number[]): number =>
242
+ values.length === 0 ? 0 : values.reduce((a, b) => a + b, 0) / values.length;
package/src/gateway.ts ADDED
@@ -0,0 +1,203 @@
1
+ // The LLM gateway: one entry point for every model call in an Ultimate app.
2
+ //
3
+ // Provider-agnostic (routes by model id), streaming, retrying with backoff on rate limits,
4
+ // budgeted, and cost-accounted in integer minor units. Everything an app does with a model
5
+ // goes through here, so budgets and accounting cannot be bypassed by a stray fetch.
6
+
7
+ import type { Money } from '@ultimat3/money';
8
+ import type { BudgetLimits, BudgetStore } from './budget';
9
+ import { BudgetLedger, currentBudget, estimateSpend, withBudget } from './budget';
10
+ import { AiProviderUnavailableError } from './errors';
11
+ import type { ModelId } from './models';
12
+ import { DEFAULT_MODEL } from './models';
13
+ import type { GenerateRequest, GenerateResult, Provider, StreamChunk } from './provider';
14
+
15
+ /**
16
+ * Response cache. Structurally satisfied by `@ultimat3/cache`'s memo/LRU tiers; declared as
17
+ * an interface so the gateway is testable with a `Map` and so caching stays optional.
18
+ */
19
+ export interface GatewayCache {
20
+ get(key: string): Promise<string | undefined> | string | undefined;
21
+ set(key: string, value: string): Promise<void> | void;
22
+ }
23
+
24
+ export interface RetryPolicy {
25
+ /** Total attempts including the first. */
26
+ readonly attempts: number;
27
+ /** First backoff in ms; doubled per attempt with full jitter. */
28
+ readonly baseDelayMs: number;
29
+ readonly maxDelayMs: number;
30
+ }
31
+
32
+ export const DEFAULT_RETRY: RetryPolicy = { attempts: 3, baseDelayMs: 500, maxDelayMs: 8_000 };
33
+
34
+ export interface CreateGatewayInput {
35
+ /** Tried in order for a given model. First provider that lists the model wins. */
36
+ readonly providers: readonly Provider[];
37
+ readonly budget?: BudgetLimits;
38
+ readonly budgetStore?: BudgetStore;
39
+ readonly cache?: GatewayCache;
40
+ readonly retry?: RetryPolicy;
41
+ readonly defaultModel?: ModelId;
42
+ /** Overridable so a test can run backoff without waiting. */
43
+ sleep?(ms: number): Promise<void>;
44
+ }
45
+
46
+ export interface Gateway {
47
+ generate(request: GenerateRequest): Promise<GenerateResult>;
48
+ stream(request: GenerateRequest): AsyncIterable<StreamChunk>;
49
+ /** Open a budget scope. Nested gateway calls inside `fn` share one ledger. */
50
+ scope<T>(input: { actorKey?: string; orgKey?: string }, fn: () => Promise<T>): Promise<T>;
51
+ /** Accumulated cost of the ambient scope, or zero outside one. */
52
+ spent(): Promise<Money>;
53
+ }
54
+
55
+ export function createGateway(input: CreateGatewayInput): Gateway {
56
+ return new GatewayImpl(input);
57
+ }
58
+
59
+ class GatewayImpl implements Gateway {
60
+ private readonly config: CreateGatewayInput;
61
+ private readonly retry: RetryPolicy;
62
+ private readonly sleep: (ms: number) => Promise<void>;
63
+
64
+ constructor(config: CreateGatewayInput) {
65
+ this.config = config;
66
+ this.retry = config.retry ?? DEFAULT_RETRY;
67
+ this.sleep = config.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
68
+ }
69
+
70
+ scope<T>(input: { actorKey?: string; orgKey?: string }, fn: () => Promise<T>): Promise<T> {
71
+ const ledger = new BudgetLedger({
72
+ limits: this.config.budget ?? {},
73
+ ...(input.actorKey !== undefined ? { actorKey: input.actorKey } : {}),
74
+ ...(input.orgKey !== undefined ? { orgKey: input.orgKey } : {}),
75
+ ...(this.config.budgetStore !== undefined ? { store: this.config.budgetStore } : {}),
76
+ });
77
+ return withBudget(ledger, fn);
78
+ }
79
+
80
+ async spent(): Promise<Money> {
81
+ const ledger = currentBudget();
82
+ if (ledger === undefined) return { minor: 0, currency: 'USD' };
83
+ return (await ledger.report()).cost;
84
+ }
85
+
86
+ async generate(request: GenerateRequest): Promise<GenerateResult> {
87
+ const model = request.model ?? this.config.defaultModel ?? DEFAULT_MODEL;
88
+ const resolved: GenerateRequest = { ...request, model };
89
+
90
+ const cacheKey = cacheKeyFor(resolved);
91
+ const cached = await this.config.cache?.get(cacheKey);
92
+ if (cached !== undefined) {
93
+ // A cache hit costs nothing, so it is deliberately NOT debited from the budget.
94
+ return JSON.parse(cached) as GenerateResult;
95
+ }
96
+
97
+ // Reserve against the ESTIMATE before spending anything — tokens AND money, since a
98
+ // cheap-in-tokens call on an expensive model is still a cost cap the app declared.
99
+ // `record` below replaces the estimate with the provider's real counts.
100
+ const ledger = currentBudget();
101
+ await ledger?.reserve(estimateSpend(resolved));
102
+
103
+ const result = await this.attempt(model, (provider) => provider.generate(resolved));
104
+ await ledger?.record(result.usage, result.cost);
105
+ // A refusal is not an answer, so it is not cached. Storing one would keep serving a decision
106
+ // the classifier might not make twice, long after the prompt that provoked it was fixed.
107
+ if (result.stopReason !== 'refusal') {
108
+ await this.config.cache?.set(cacheKey, JSON.stringify(result));
109
+ }
110
+ return result;
111
+ }
112
+
113
+ async *stream(request: GenerateRequest): AsyncIterable<StreamChunk> {
114
+ const model = request.model ?? this.config.defaultModel ?? DEFAULT_MODEL;
115
+ const resolved: GenerateRequest = { ...request, model };
116
+ const ledger = currentBudget();
117
+ await ledger?.reserve(estimateSpend(resolved));
118
+
119
+ // A stream is not retried mid-flight: the consumer has already seen tokens, and
120
+ // replaying from the top would duplicate them. Only the handshake retries.
121
+ const provider = this.providerFor(model);
122
+ for await (const chunk of provider.stream(resolved)) {
123
+ if (chunk.type === 'done') await ledger?.record(chunk.result.usage, chunk.result.cost);
124
+ yield chunk;
125
+ }
126
+ }
127
+
128
+ private providerFor(model: ModelId): Provider {
129
+ const provider = this.config.providers.find((p) => p.models.includes(model));
130
+ if (provider === undefined) {
131
+ throw new AiProviderUnavailableError({
132
+ model,
133
+ attempts: this.config.providers.map((p) => `${p.name}: does not serve ${model}`),
134
+ });
135
+ }
136
+ return provider;
137
+ }
138
+
139
+ /**
140
+ * Try every provider that serves `model`, retrying each on a retryable failure with
141
+ * exponential backoff plus full jitter (jitter matters: synchronised retries from N
142
+ * workers reproduce the rate limit they are backing off from).
143
+ */
144
+ private async attempt<T>(model: ModelId, call: (provider: Provider) => Promise<T>): Promise<T> {
145
+ const candidates = this.config.providers.filter((p) => p.models.includes(model));
146
+ const failures: string[] = [];
147
+ if (candidates.length === 0) {
148
+ throw new AiProviderUnavailableError({ model, attempts: [`no provider serves ${model}`] });
149
+ }
150
+
151
+ for (const provider of candidates) {
152
+ for (let attempt = 1; attempt <= this.retry.attempts; attempt += 1) {
153
+ try {
154
+ return await call(provider);
155
+ } catch (error) {
156
+ failures.push(`${provider.name}#${attempt}: ${messageOf(error)}`);
157
+ if (!isRetryable(error) || attempt === this.retry.attempts) break;
158
+ await this.sleep(backoffMs(this.retry, attempt));
159
+ }
160
+ }
161
+ }
162
+ throw new AiProviderUnavailableError({ model, attempts: failures });
163
+ }
164
+ }
165
+
166
+ /** Full jitter: a uniform pick from [0, exponential], capped. */
167
+ export function backoffMs(policy: RetryPolicy, attempt: number): number {
168
+ const ceiling = Math.min(policy.baseDelayMs * 2 ** (attempt - 1), policy.maxDelayMs);
169
+ return Math.floor(Math.random() * ceiling);
170
+ }
171
+
172
+ /**
173
+ * Retryable = the request was well formed and the provider was momentarily unable. A 400 is
174
+ * never retried: the same body produces the same rejection and only burns the budget.
175
+ */
176
+ export function isRetryable(error: unknown): boolean {
177
+ if (typeof error !== 'object' || error === null) return false;
178
+ const e = error as { status?: unknown; code?: unknown };
179
+ if (typeof e.status === 'number') return e.status === 429 || e.status >= 500;
180
+ return e.code === 'ETIMEDOUT' || e.code === 'ECONNRESET';
181
+ }
182
+
183
+ function messageOf(error: unknown): string {
184
+ if (error instanceof Error) return error.message;
185
+ return String(error);
186
+ }
187
+
188
+ /**
189
+ * Cache key. Every field that changes the answer is in it — a key that ignores `effort` or
190
+ * `system` would serve one prompt's answer for another.
191
+ */
192
+ export function cacheKeyFor(request: GenerateRequest): string {
193
+ return JSON.stringify({
194
+ model: request.model,
195
+ system: request.system ?? '',
196
+ messages: request.messages,
197
+ maxTokens: request.maxTokens,
198
+ effort: request.effort ?? 'high',
199
+ thinking: request.thinking ?? 'adaptive',
200
+ tools: request.tools?.map((t) => t.name) ?? [],
201
+ stop: request.stopSequences ?? [],
202
+ });
203
+ }
package/src/index.ts ADDED
@@ -0,0 +1,187 @@
1
+ // Public API of @ultimat3/ai. Explicit — a wildcard barrel would leak internals an app
2
+ // could depend on, and the gateway's guarantees only hold if every call goes through it.
3
+
4
+ /** Re-exported so an `llm` file needs one import, not two. Same object as schema's. */
5
+ export type { Infer } from '@ultimat3/schema';
6
+ export { t } from '@ultimat3/schema';
7
+ export type {
8
+ BudgetLedgerInput,
9
+ BudgetLimits,
10
+ BudgetReport,
11
+ BudgetStore,
12
+ SpendEstimate,
13
+ } from './budget';
14
+ export {
15
+ BudgetLedger,
16
+ currentBudget,
17
+ estimateSpend,
18
+ MemoryBudgetStore,
19
+ withBudget,
20
+ } from './budget';
21
+ export type { Embedder, HashEmbedderInput } from './embeddings';
22
+ export {
23
+ cosine,
24
+ embedBatched,
25
+ embedOne,
26
+ fnv1a,
27
+ HashEmbedder,
28
+ normalize,
29
+ tokenize,
30
+ } from './embeddings';
31
+ export type { AiErrorCode } from './errors';
32
+ export {
33
+ AI_ERROR_CODES,
34
+ AI_ERROR_TITLES,
35
+ AiBudgetExceededError,
36
+ AiGatewayMissingError,
37
+ AiKeyMissingError,
38
+ AiPromptRenderError,
39
+ AiPromptVersionError,
40
+ AiProviderUnavailableError,
41
+ AiRequestInvalidError,
42
+ AiTransportError,
43
+ EmbedderDimMismatchError,
44
+ EvalBaselineInvalidError,
45
+ EvalBaselineMissingError,
46
+ EvalMissingError,
47
+ EvalRecordingError,
48
+ EvalThresholdError,
49
+ LlmOutputInvalidError,
50
+ LlmRefusedError,
51
+ LlmTruncatedError,
52
+ VectorDimMismatchError,
53
+ VectorScopeWidenedError,
54
+ } from './errors';
55
+ export type { EvalBaseline, Regression } from './eval-baseline';
56
+ export {
57
+ baselinePath,
58
+ describeRegression,
59
+ OVERALL,
60
+ RECORD_ENV,
61
+ readBaseline,
62
+ recordingBaselines,
63
+ regressionsAgainst,
64
+ writeBaseline,
65
+ } from './eval-baseline';
66
+ export type {
67
+ CaseResult,
68
+ DefineEvalInput,
69
+ Eval,
70
+ EvalCase,
71
+ EvalFact,
72
+ EvalResult,
73
+ } from './evals';
74
+ export {
75
+ baselineFrom,
76
+ defineEval,
77
+ describeEvals,
78
+ promptsWithoutEvals,
79
+ resetEvals,
80
+ } from './evals';
81
+ export type { CreateGatewayInput, Gateway, GatewayCache, RetryPolicy } from './gateway';
82
+ export { backoffMs, cacheKeyFor, createGateway, DEFAULT_RETRY, isRetryable } from './gateway';
83
+ export type {
84
+ LlmBudget,
85
+ LlmCache,
86
+ LlmDef,
87
+ LlmSemanticCache,
88
+ LlmVarsArgs,
89
+ } from './llm';
90
+ export { llm } from './llm';
91
+ export type { Effort, ModelId, ModelReasoning, ModelSpec, ThinkingMode } from './models';
92
+ export { DEFAULT_MODEL, EFFORTS, MODEL_IDS, MODELS, reasoningBody } from './models';
93
+ export type { PgVectorStoreInput } from './pg-vector';
94
+ export { PgVectorStore } from './pg-vector';
95
+ export type {
96
+ PgHybridArgs,
97
+ PgSearchArgs,
98
+ PgVectorRowInput,
99
+ PgVectorTable,
100
+ } from './pg-vector-sql';
101
+ export {
102
+ conditionsSql,
103
+ ddlSql,
104
+ deleteSql,
105
+ hybridSql,
106
+ searchSql,
107
+ textSql,
108
+ upsertSql,
109
+ vectorLiteral,
110
+ } from './pg-vector-sql';
111
+ export type { DefinePromptInput, Prompt, PromptVars } from './prompt';
112
+ export {
113
+ contentHash,
114
+ definePrompt,
115
+ describePrompts,
116
+ getPrompt,
117
+ promptVersions,
118
+ resetPrompts,
119
+ } from './prompt';
120
+ export type {
121
+ AiMessage,
122
+ AnthropicProviderInput,
123
+ EchoProviderInput,
124
+ GenerateRequest,
125
+ GenerateResult,
126
+ Provider,
127
+ StopDetails,
128
+ StopReason,
129
+ StreamChunk,
130
+ TokenUsage,
131
+ } from './provider';
132
+ export {
133
+ AnthropicProvider,
134
+ costOf,
135
+ EchoProvider,
136
+ estimateCost,
137
+ estimateInputTokens,
138
+ estimateTextTokens,
139
+ estimateTokens,
140
+ parseMessage,
141
+ requiresStreaming,
142
+ STREAM_ONLY_MAX_TOKENS,
143
+ totalTokens,
144
+ } from './provider';
145
+ export type {
146
+ AssembledContext,
147
+ Chunk,
148
+ ChunkInput,
149
+ Reranker,
150
+ RetrieveInput,
151
+ } from './rag';
152
+ export { assembleContext, chunk, indexDocument, passthroughReranker, retrieve } from './rag';
153
+ export type { RemoteEmbedderInput } from './remote-embedder';
154
+ export { RemoteEmbedder } from './remote-embedder';
155
+ export type { AiRuntimeInput } from './runtime';
156
+ export { aiEmbedder, aiGateway, configureAi, resetAiRuntime, semanticCacheFor } from './runtime';
157
+ export type { Scorer } from './scorers';
158
+ export {
159
+ contains,
160
+ exact,
161
+ jsonSchemaValid,
162
+ jsonValid,
163
+ llmJudge,
164
+ numericTolerance,
165
+ } from './scorers';
166
+ export type {
167
+ JsonSchema,
168
+ LlmTool,
169
+ LlmToolCall,
170
+ LlmToolResult,
171
+ ProjectableAction,
172
+ } from './tools';
173
+ export { runLlmToolCall, toLlmTool, toLlmTools } from './tools';
174
+ export type {
175
+ HybridSearchInput,
176
+ MemoryVectorStoreInput,
177
+ MetadataFilter,
178
+ SearchHit,
179
+ StoredRecord,
180
+ VectorRecord,
181
+ VectorStore,
182
+ } from './vector';
183
+ export { fuse, MemoryVectorStore } from './vector';
184
+ export type { VectorScope } from './vector-scope';
185
+ export { NO_TENANT, narrowScope, scopeAdmits, tenantOf, UNSCOPED } from './vector-scope';
186
+ export type { StreamState } from './wire';
187
+ export { parseStopDetails, ZERO_USAGE } from './wire';