@ultimat3/ai 24.0.0 → 25.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -12,29 +12,49 @@ export type { AgentJobOptions } from './agent-job';
12
12
  export { agentJob } from './agent-job';
13
13
  export type {
14
14
  BudgetKeys,
15
+ BudgetLedger,
15
16
  BudgetLedgerInput,
16
17
  BudgetLimits,
17
18
  BudgetReport,
18
19
  BudgetStore,
19
20
  BudgetTake,
21
+ MemoryBudgetStore,
20
22
  SpendEstimate,
21
23
  } from './budget';
22
24
  export {
23
- BudgetLedger,
24
25
  budgetKeysFor,
25
26
  currentBudget,
26
27
  estimateSpend,
27
- MemoryBudgetStore,
28
+ memoryBudgetStore,
28
29
  withBudget,
29
30
  } from './budget';
30
- export type { Embedder, HashEmbedderInput } from './embeddings';
31
+ export type {
32
+ AiDocumentBlock,
33
+ AiDocumentSource,
34
+ AiImageBlock,
35
+ AiImageSource,
36
+ AiMediaBlock,
37
+ ImageMediaType,
38
+ } from './content-blocks';
39
+ export {
40
+ IMAGE_MEDIA_TYPES,
41
+ IMAGE_TOKEN_ESTIMATE,
42
+ isMediaBlock,
43
+ MAX_DOCUMENT_BASE64_CHARS,
44
+ MAX_IMAGE_BASE64_CHARS,
45
+ } from './content-blocks';
46
+ export type { ContentRefusal } from './content-errors';
47
+ export { AiContentUnsupportedError } from './content-errors';
48
+ export type { EchoProvider, EchoProviderInput } from './echo-provider';
49
+ export { echoProvider } from './echo-provider';
50
+ export type { Embedder, HashEmbedder, HashEmbedderInput } from './embeddings';
31
51
  export {
32
52
  cosine,
33
53
  embedBatched,
34
54
  embedOne,
35
- HashEmbedder,
36
- normalize,
37
- tokenize,
55
+ hashEmbedder,
56
+ normalizeVector,
57
+ wordTokens,
38
58
  } from './embeddings';
39
59
  export type { AiErrorCode } from './errors';
40
60
  export {
@@ -94,8 +114,14 @@ export {
94
114
  resetEvals,
95
115
  } from './evals';
96
116
  export type { AiFetch } from './fetch-seam';
97
- export type { CreateGatewayInput, Gateway, GatewayCache, RetryPolicy } from './gateway';
98
- export { backoffMs, cacheKeyFor, createGateway, DEFAULT_RETRY, isRetryable } from './gateway';
117
+ export type { Gateway, GatewayCache, ProviderGatewayInput, RetryPolicy } from './gateway';
118
+ export {
119
+ backoffMs,
120
+ DEFAULT_GATEWAY_RETRY,
121
+ isRetryable,
122
+ promptCacheKey,
123
+ providerGateway,
124
+ } from './gateway';
99
125
  export type { HiveDef, HiveSplitArgs } from './hive';
100
126
  export { hive } from './hive';
101
127
  export { HiveEmptyError } from './hive-errors';
@@ -104,11 +130,19 @@ export type { LlmAction, LlmBudget, LlmDef, LlmVarsArgs } from './llm';
104
130
  export { llm } from './llm';
105
131
  export type { LlmCache, LlmScopeArgs, LlmSemanticCache } from './llm-cache';
106
132
  export type { LlmStreamChunk } from './llm-stream';
107
- export type { Effort, ModelId, ModelReasoning, ModelSpec, ThinkingMode } from './models';
133
+ export type { ModelSource } from './model-resolve';
134
+ export { AiModelUnresolvedError } from './model-resolve';
135
+ export type {
136
+ ContentKind,
137
+ DeclaredReasoning,
138
+ Effort,
139
+ ModelId,
140
+ ModelReasoning,
141
+ ModelSpec,
142
+ ThinkingMode,
143
+ } from './models';
108
144
  export {
109
- ANTHROPIC_MODEL_IDS,
110
145
  assertModel,
111
- DEFAULT_MODEL,
112
146
  EFFORTS,
113
147
  isModelRegistered,
114
148
  modelIds,
@@ -118,11 +152,10 @@ export {
118
152
  registerModel,
119
153
  resetModels,
120
154
  } from './models';
121
- export { OPENAI_MODEL_IDS, registerOpenAiModels } from './openai-models';
122
155
  export type { OpenAiProviderInput } from './openai-provider';
123
156
  export { openAiProvider } from './openai-provider';
124
- export type { PgVectorStoreInput } from './pg-vector';
125
- export { PgVectorStore } from './pg-vector';
157
+ export type { PostgresVectorStore, PostgresVectorStoreInput } from './pg-vector';
158
+ export { postgresVectorStore } from './pg-vector';
126
159
  export type {
127
160
  PgHybridArgs,
128
161
  PgSearchArgs,
@@ -137,18 +170,19 @@ export {
137
170
  } from './pg-vector-sql';
138
171
  export type { DefinePromptInput, Prompt, PromptVars } from './prompt';
139
172
  export {
140
- contentHash,
141
173
  definePrompt,
142
174
  describePrompts,
143
175
  getPrompt,
176
+ promptHash,
144
177
  promptVersions,
145
178
  resetPrompts,
146
179
  } from './prompt';
180
+ export { promptFences } from './prompt-fence';
147
181
  export type {
148
182
  AiContentBlock,
149
183
  AiMessage,
184
+ AnthropicProvider,
150
185
  AnthropicProviderInput,
151
- EchoProviderInput,
152
186
  GenerateRequest,
153
187
  GenerateResult,
154
188
  Provider,
@@ -158,9 +192,8 @@ export type {
158
192
  TokenUsage,
159
193
  } from './provider';
160
194
  export {
161
- AnthropicProvider,
195
+ anthropicProvider,
162
196
  costOf,
163
- EchoProvider,
164
197
  estimateCost,
165
198
  estimateInputTokens,
166
199
  estimateTextTokens,
@@ -179,17 +212,22 @@ export type {
179
212
  Reranker,
180
213
  RetrieveInput,
181
214
  } from './rag';
182
- export { assembleContext, chunk, indexDocument, passthroughReranker, retrieve } from './rag';
215
+ export {
216
+ assembleContext,
217
+ chunkDocument,
218
+ indexDocument,
219
+ passthroughReranker,
220
+ retrieve,
221
+ } from './rag';
183
222
  export { assertNoSecrets } from './redaction';
184
- export type { RemoteEmbedderInput } from './remote-embedder';
185
- export { RemoteEmbedder } from './remote-embedder';
223
+ export type { RemoteEmbedder, RemoteEmbedderInput } from './remote-embedder';
224
+ export { remoteEmbedder } from './remote-embedder';
186
225
  export type { AiRuntimeInput, Redactor } from './runtime';
187
226
  export {
188
227
  aiEmbedder,
189
228
  aiGateway,
190
229
  aiRedactor,
191
230
  configureAi,
192
- MAX_SEMANTIC_CACHE_SCOPES,
193
231
  semanticCacheFor,
194
232
  } from './runtime';
195
233
  export type { Scorer } from './scorers';
@@ -212,6 +250,7 @@ export type {
212
250
  export { asProjectableAction, runLlmToolCall, toLlmTool, toLlmTools } from './tools';
213
251
  export type {
214
252
  HybridSearchInput,
253
+ MemoryVectorStore,
215
254
  MemoryVectorStoreInput,
216
255
  MetadataFilter,
217
256
  SearchHit,
@@ -219,7 +258,7 @@ export type {
219
258
  VectorRecord,
220
259
  VectorStore,
221
260
  } from './vector';
222
- export { fuse, MemoryVectorStore } from './vector';
261
+ export { fuse, memoryVectorStore } from './vector';
223
262
  export type { VectorScope } from './vector-scope';
224
- export { NO_TENANT, tenantOf, UNSCOPED, VectorUnscopedError } from './vector-scope';
263
+ export { NO_VECTOR_TENANT, tenantOf, UNSCOPED, VectorUnscopedError } from './vector-scope';
225
264
  export type { StreamState } from './wire';
package/src/llm-cache.ts CHANGED
@@ -74,7 +74,7 @@ export interface PromptCache {
74
74
  * writing its own `scope`, and the locale has to survive that — so it lives in the unconditional
75
75
  * half of the store key, beside the prompt hash.
76
76
  *
77
- * `ctx.actor` is never absent (`createContext` defaults it to `anonymousActor()`), so every
77
+ * `ctx.actor` is never absent (`ctxOf` defaults it to `anonymousActor()`), so every
78
78
  * anonymous caller shares one partition — which is what the anonymous actor already means
79
79
  * everywhere else in the framework.
80
80
  */
package/src/llm.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * The eight primitives are the whole vocabulary. A model call is a server-authoritative
5
5
  * operation with an input schema, an output schema and a policy, which is the definition of
6
6
  * an `action` — so this file is a FACTORY over `action()`, never a new kind of thing. That is
7
- * why `summarize.tool()`, `.openapi()`, `.client()`, `.job()` and `.contract()` all exist
7
+ * why `summarize.openapi()`, `.client()`, `.job()`, `.contract()` and its MCP tool all exist
8
8
  * without a line here: the value is an action, so it projects like one, and an app gains an
9
9
  * MCP tool backed by a model the moment it declares one.
10
10
  *
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * `.stream()` is the same action over a different transport, and it is here rather than beside
16
16
  * the gateway for one reason: everything that makes `llm()` worth using — policy, input parse,
17
- * budget scope, semantic cache, span, `.tool()` — is lost the moment a feature has to reach past
17
+ * budget scope, semantic cache, span, MCP tool — is lost the moment a feature has to reach past
18
18
  * it to `aiGateway()` for tokens on a screen. `./llm-stream.ts` holds the plumbing and states the
19
19
  * two decisions a stream forces (no repair turn, budget reserved exactly as before); what a
20
20
  * streamed answer must satisfy is decided in this file, next to the non-streaming version of the
@@ -29,7 +29,7 @@ import type { Money } from '@ultimat3/money';
29
29
  import type { InferInput, InferOutput, StandardSchemaV1 } from '@ultimat3/schema';
30
30
  import { formatIssues, validateAsync } from '@ultimat3/schema';
31
31
  import type { BudgetLimits } from './budget';
32
- import { budgetKeysFor, currentBudget, withBudget } from './budget';
32
+ import { budgetKeysFor, currentBudget, screenedMoney, withBudget } from './budget';
33
33
  import {
34
34
  LlmOutputInvalidError,
35
35
  LlmRefusedError,
@@ -41,8 +41,9 @@ import type { LlmCache } from './llm-cache';
41
41
  import { openCache } from './llm-cache';
42
42
  import type { LlmSink, LlmStreamChunk } from './llm-stream';
43
43
  import { currentLlmSink, llmStream, streamOneTurn, withLlmSink } from './llm-stream';
44
+ import { modelForCall } from './model-resolve';
44
45
  import type { ModelId } from './models';
45
- import { DEFAULT_MODEL, moreCapableThan } from './models';
46
+ import { moreCapableThan } from './models';
46
47
  import type { Prompt, PromptVars } from './prompt';
47
48
  import type { AiMessage, GenerateRequest, GenerateResult } from './provider';
48
49
  import { isTruncated } from './provider';
@@ -101,7 +102,7 @@ export interface LlmDef<
101
102
  }
102
103
 
103
104
  /**
104
- * An `action` with one extra way to be called. Every projection an action has, it has — `.tool()`,
105
+ * An `action` with one extra way to be called. Every projection an action has, it has — its MCP tool,
105
106
  * `.openapi()`, `.client()`, `.job()`, `.contract()` — plus a transport for the case an action's
106
107
  * single return value cannot serve: text on a screen before the answer is finished.
107
108
  */
@@ -191,7 +192,10 @@ async function generate<
191
192
  // The prompt ref is the identity every failure here is about, and unlike the action's
192
193
  // export name it exists before registration and can never twin.
193
194
  const name = prompt.ref;
194
- const model = def.model ?? prompt.model ?? DEFAULT_MODEL;
195
+ // Screened first: a bad declaration is refused whether or not a gateway exists.
196
+ const limits = limitsOf(def.budget);
197
+ // The declaration, its prompt, then the gateway's `defaultModel` — the app's three places.
198
+ const model = modelForCall('llm', name, def.model, prompt.model);
195
199
  // `vars()` is the one declared place a model call loads data, so it is the one place the
196
200
  // framework can refuse a `Secret` and the one place an app's redactor can see the row before it
197
201
  // leaves the process. Both run here, between the load and the request, and neither is optional
@@ -249,8 +253,6 @@ async function generate<
249
253
  // A ledger derived from the ambient one, so a per-call budget can only TIGHTEN the actor
250
254
  // and org ceilings this call runs inside, never widen them. The gateway reserves against
251
255
  // it before the provider is touched — that is where `X_AI_BUDGET_EXCEEDED` comes from.
252
- // Screened first, as before: a bad declaration is refused whether or not a gateway exists.
253
- const limits = limitsOf(def.budget);
254
256
  // Rooted in the GATEWAY's own ceilings when no scope is open — an empty root ignored them.
255
257
  const gateway = aiGateway(name);
256
258
  // Keyed on the CALLER, so the gateway's `actor` / `org` ceilings count this call against them.
@@ -280,7 +282,7 @@ async function generate<
280
282
  // The fix names a model the caller can paste, and only ever a MORE capable one:
281
283
  // "the first id that differs" answered a refusal on the default model with the next
282
284
  // entry down the ladder, which is a retry that cannot succeed.
283
- alternative: moreCapableThan(result.model),
285
+ alternative: moreCapableThan(result.model, request),
284
286
  category: result.stopDetails?.category,
285
287
  explanation: result.stopDetails?.explanation,
286
288
  });
@@ -346,7 +348,7 @@ async function streamedAnswer<TOutput extends StandardSchemaV1>(
346
348
  throw new LlmRefusedError({
347
349
  prompt: name,
348
350
  model: result.model,
349
- alternative: moreCapableThan(result.model),
351
+ alternative: moreCapableThan(result.model, request),
350
352
  category: result.stopDetails?.category,
351
353
  explanation: result.stopDetails?.explanation,
352
354
  });
@@ -392,7 +394,9 @@ function limitsOf(budget: LlmBudget | undefined): BudgetLimits {
392
394
  ...(budget?.tokensIn === undefined
393
395
  ? {}
394
396
  : { tokensIn: finiteCount('llm', 'budget.tokensIn', budget.tokensIn) }),
395
- ...(budget?.costPerCall === undefined ? {} : { costPerCall: budget.costPerCall }),
397
+ ...(budget?.costPerCall === undefined
398
+ ? {}
399
+ : { costPerCall: screenedMoney('llm', 'budget.costPerCall', budget.costPerCall) }),
396
400
  };
397
401
  }
398
402
 
@@ -0,0 +1,79 @@
1
+ // Which model a call runs on, decided in ONE place. The app names it in one of three places — the
2
+ // declaration's `model`, its prompt's, `providerGateway({ defaultModel })` — and the framework has
3
+ // no fourth: a call that names none is refused here, naming all three, never routed to a vendor.
4
+
5
+ import { UltimateError } from '@ultimat3/core';
6
+ import type { ModelId } from './models';
7
+ import { aiGateway } from './runtime';
8
+
9
+ /** Where a model was resolved. Bounded, and the first word of the refusal's cause. */
10
+ export type ModelSite = 'llm' | 'agent' | 'gateway' | 'provider' | 'echo-provider';
11
+
12
+ /** The three places, spelled once so the refusal and the docs cannot drift apart. */
13
+ const PLACES =
14
+ 'model: modelId on the llm()/agent() declaration, model: modelId in its definePrompt, or providerGateway({ …, defaultModel: modelId })';
15
+
16
+ /**
17
+ * The first declared candidate, in the caller's precedence order (a declaration, then its prompt,
18
+ * then the gateway's `defaultModel`). None declared is `X_AI_MODEL_UNRESOLVED`: which model, and so
19
+ * which vendor, is the app's call and never the framework's.
20
+ */
21
+ export function resolveModel(
22
+ site: ModelSite,
23
+ ...candidates: readonly (ModelId | undefined)[]
24
+ ): ModelId {
25
+ for (const candidate of candidates) if (candidate !== undefined) return candidate;
26
+ throw new AiModelUnresolvedError(site);
27
+ }
28
+
29
+ /** A call that named no model: its own code, because the fix is a declaration, not a request. */
30
+ export class AiModelUnresolvedError extends UltimateError {
31
+ constructor(site: ModelSite) {
32
+ super({
33
+ code: 'X_AI_MODEL_UNRESOLVED',
34
+ cause: `${site} resolved no model: no declaration, prompt or providerGateway({ defaultModel }) named one, and the framework chooses none (modelId: an id your app registerModel-ed and a configured provider lists)`,
35
+ fix: `${PLACES} # one of the three`,
36
+ });
37
+ }
38
+ }
39
+
40
+ /**
41
+ * The model an `llm()` or `agent()` call runs on. The gateway is asked only when neither the
42
+ * declaration nor its prompt names one — and with none installed that is `X_AI_GATEWAY_MISSING`
43
+ * for `prompt`, because the third place does not exist yet: the boot call is the whole fix.
44
+ */
45
+ export function modelForCall(
46
+ site: 'llm' | 'agent',
47
+ prompt: string,
48
+ declared: ModelId | undefined,
49
+ promptModel: ModelId | undefined,
50
+ ): ModelId {
51
+ if (declared !== undefined || promptModel !== undefined) {
52
+ return resolveModel(site, declared, promptModel);
53
+ }
54
+ return resolveModel(site, aiGateway(prompt).defaultModel);
55
+ }
56
+
57
+ /** Which of the app's three places a model came from. */
58
+ export type ModelSource = 'declaration' | 'prompt' | 'gateway';
59
+
60
+ const SOURCES: readonly ModelSource[] = ['declaration', 'prompt', 'gateway'];
61
+
62
+ /**
63
+ * The same answer as `resolveModel`, over the same three candidates in the same order, WITHOUT
64
+ * refusing — and with where it came from. For a fact published about a declaration
65
+ * (`describeAgents()`): a reader describing an app is not a call, so a declaration that names no
66
+ * model yet (the gateway is installed later) is described as `undefined`, never thrown over.
67
+ */
68
+ export function describedModel(
69
+ declared: ModelId | undefined,
70
+ prompt: ModelId | undefined,
71
+ gateway: ModelId | undefined,
72
+ ): { readonly model: ModelId | undefined; readonly from: ModelSource | undefined } {
73
+ const candidates = [declared, prompt, gateway];
74
+ for (const [at, candidate] of candidates.entries()) {
75
+ const from = SOURCES[at];
76
+ if (candidate !== undefined && from !== undefined) return { model: candidate, from };
77
+ }
78
+ return { model: undefined, from: undefined };
79
+ }