@untangle-code/embeddings-ollama 0.0.0-stage → 0.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.
Files changed (75) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +90 -0
  3. package/README.md +187 -2
  4. package/THIRD_PARTY_LICENSES.md +24 -0
  5. package/dist/backoff.d.ts +43 -0
  6. package/dist/backoff.d.ts.map +1 -0
  7. package/dist/backoff.js +38 -0
  8. package/dist/backoff.js.map +1 -0
  9. package/dist/cache.d.ts +17 -0
  10. package/dist/cache.d.ts.map +1 -0
  11. package/dist/cache.js +17 -0
  12. package/dist/cache.js.map +1 -0
  13. package/dist/concurrency.d.ts +11 -0
  14. package/dist/concurrency.d.ts.map +1 -0
  15. package/dist/concurrency.js +31 -0
  16. package/dist/concurrency.js.map +1 -0
  17. package/dist/endpoint.d.ts +16 -0
  18. package/dist/endpoint.d.ts.map +1 -0
  19. package/dist/endpoint.js +32 -0
  20. package/dist/endpoint.js.map +1 -0
  21. package/dist/http.d.ts +54 -0
  22. package/dist/http.d.ts.map +1 -0
  23. package/dist/http.js +136 -0
  24. package/dist/http.js.map +1 -0
  25. package/dist/index.d.ts +13 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +18 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/inflight.d.ts +49 -0
  30. package/dist/inflight.d.ts.map +1 -0
  31. package/dist/inflight.js +87 -0
  32. package/dist/inflight.js.map +1 -0
  33. package/dist/ollama-api.d.ts +45 -0
  34. package/dist/ollama-api.d.ts.map +1 -0
  35. package/dist/ollama-api.js +40 -0
  36. package/dist/ollama-api.js.map +1 -0
  37. package/dist/provider.d.ts +111 -0
  38. package/dist/provider.d.ts.map +1 -0
  39. package/dist/provider.js +374 -0
  40. package/dist/provider.js.map +1 -0
  41. package/dist/readiness.d.ts +31 -0
  42. package/dist/readiness.d.ts.map +1 -0
  43. package/dist/readiness.js +105 -0
  44. package/dist/readiness.js.map +1 -0
  45. package/dist/sleep.d.ts +8 -0
  46. package/dist/sleep.d.ts.map +1 -0
  47. package/dist/sleep.js +20 -0
  48. package/dist/sleep.js.map +1 -0
  49. package/dist/truncation.d.ts +30 -0
  50. package/dist/truncation.d.ts.map +1 -0
  51. package/dist/truncation.js +39 -0
  52. package/dist/truncation.js.map +1 -0
  53. package/dist/types.d.ts +108 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +7 -0
  56. package/dist/types.js.map +1 -0
  57. package/dist/validation.d.ts +2 -0
  58. package/dist/validation.d.ts.map +1 -0
  59. package/dist/validation.js +16 -0
  60. package/dist/validation.js.map +1 -0
  61. package/package.json +62 -3
  62. package/src/backoff.ts +73 -0
  63. package/src/cache.ts +30 -0
  64. package/src/concurrency.ts +37 -0
  65. package/src/endpoint.ts +31 -0
  66. package/src/http.ts +168 -0
  67. package/src/index.ts +21 -0
  68. package/src/inflight.ts +102 -0
  69. package/src/ollama-api.ts +93 -0
  70. package/src/provider.ts +477 -0
  71. package/src/readiness.ts +203 -0
  72. package/src/sleep.ts +27 -0
  73. package/src/truncation.ts +41 -0
  74. package/src/types.ts +132 -0
  75. package/src/validation.ts +23 -0
@@ -0,0 +1,374 @@
1
+ import { buildEmbeddingProfileId, isUntangleError, sha256Hex } from "@untangle-code/domain";
2
+ import { embeddingUnavailableError, jobCancelledError } from "@untangle-code/observability";
3
+ import { withRetry } from "./backoff.js";
4
+ import { InMemoryEmbeddingCache } from "./cache.js";
5
+ import { mapWithConcurrency } from "./concurrency.js";
6
+ import { isRequestCancellation, OllamaHttpClient, OllamaHttpError } from "./http.js";
7
+ import { createDeferred, InFlightRegistry, raceWithSignal } from "./inflight.js";
8
+ import { embed } from "./ollama-api.js";
9
+ import { runReadinessSequence } from "./readiness.js";
10
+ import { realSleep } from "./sleep.js";
11
+ import { wouldProviderTruncate } from "./truncation.js";
12
+ import { DEFAULT_BASE_BACKOFF_MS, DEFAULT_MAX_BACKOFF_MS, DEFAULT_MAX_RETRIES } from "./types.js";
13
+ import { assertVectorDimension } from "./validation.js";
14
+ function isRetryableEmbedError(error) {
15
+ return error instanceof OllamaHttpError && error.retryable;
16
+ }
17
+ /** Same shape as `cache.ts`'s internal key string — local to this module, used only for in-flight bookkeeping. */
18
+ function cacheKeyString(key) {
19
+ return `${key.embeddingProfileId}:${key.preparedTextHash}`;
20
+ }
21
+ /** Splits `items` into consecutive slices of at most `size` (the last one possibly shorter). */
22
+ function chunkIntoSlices(items, size) {
23
+ if (items.length === 0) {
24
+ return [];
25
+ }
26
+ const slices = [];
27
+ for (let start = 0; start < items.length; start += size) {
28
+ slices.push(items.slice(start, start + size));
29
+ }
30
+ return slices;
31
+ }
32
+ /** Normalizes any thrown value into the `UntangleError` this package's public API promises (rule 8). */
33
+ function normalizeError(error, signal) {
34
+ if (isUntangleError(error)) {
35
+ return error;
36
+ }
37
+ if (isRequestCancellation(error, signal)) {
38
+ return jobCancelledError("Ollama embedding operation was cancelled.", {
39
+ cause: error,
40
+ retryable: false,
41
+ });
42
+ }
43
+ if (error instanceof OllamaHttpError) {
44
+ return embeddingUnavailableError(error.message, {
45
+ cause: error,
46
+ retryable: false,
47
+ context: error.status === undefined ? {} : { status: error.status },
48
+ });
49
+ }
50
+ return embeddingUnavailableError(`Unexpected Ollama embedding provider error: ${String(error)}`, {
51
+ cause: error,
52
+ retryable: false,
53
+ });
54
+ }
55
+ async function guarded(signal, fn) {
56
+ try {
57
+ signal?.throwIfAborted();
58
+ return await fn();
59
+ }
60
+ catch (error) {
61
+ throw normalizeError(error, signal);
62
+ }
63
+ }
64
+ export class OllamaEmbeddingProvider {
65
+ profile;
66
+ profileId;
67
+ config;
68
+ clock;
69
+ random;
70
+ cache;
71
+ sleep;
72
+ httpClient;
73
+ backoffConfig;
74
+ cachedReadiness;
75
+ resolvedEndpointUrl;
76
+ readyPromise;
77
+ /**
78
+ * One shared fetch-and-cache operation per still-uncached cache key
79
+ * (spec.md §9.4, WP-78): concurrent `embedDocuments`/`embedQuery` calls
80
+ * that race for the same not-yet-cached text join the SAME `/api/embed`
81
+ * request instead of each starting their own.
82
+ */
83
+ inFlightEmbeds = new InFlightRegistry();
84
+ constructor(config, deps) {
85
+ this.config = config;
86
+ this.profile = config.profile;
87
+ this.profileId = buildEmbeddingProfileId(config.profile);
88
+ this.clock = deps.clock;
89
+ this.random = deps.random;
90
+ this.cache = deps.cache ?? new InMemoryEmbeddingCache();
91
+ this.sleep = deps.sleep ?? realSleep;
92
+ this.httpClient = new OllamaHttpClient({
93
+ requestTimeoutMs: config.requestTimeoutMs,
94
+ maxConnections: config.maxConnections,
95
+ apiKeyEnv: config.apiKeyEnv,
96
+ fetchImpl: deps.fetchImpl,
97
+ });
98
+ this.backoffConfig = {
99
+ maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
100
+ baseDelayMs: config.baseBackoffMs ?? DEFAULT_BASE_BACKOFF_MS,
101
+ maxDelayMs: config.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS,
102
+ };
103
+ }
104
+ async ensureReady(options) {
105
+ return guarded(options?.signal, async () => {
106
+ await this.ensureReadyOnce(options?.signal);
107
+ // Non-null: ensureReadyOnce either already resolved it or just did.
108
+ return this.cachedReadiness;
109
+ });
110
+ }
111
+ async embedDocuments(preparedTexts, options) {
112
+ return guarded(options?.signal, async () => {
113
+ if (preparedTexts.length === 0) {
114
+ return { vectors: [], truncated: [] };
115
+ }
116
+ await this.ensureReadyOnce(options?.signal);
117
+ return this.embedTexts(preparedTexts, options?.signal, options?.onBatchProgress);
118
+ });
119
+ }
120
+ async embedQuery(preparedText, options) {
121
+ return guarded(options?.signal, async () => {
122
+ await this.ensureReadyOnce(options?.signal);
123
+ const { vectors } = await this.embedTexts([preparedText], options?.signal);
124
+ return vectors[0];
125
+ });
126
+ }
127
+ /**
128
+ * Runs the readiness sequence exactly once per instance; concurrent
129
+ * callers (including through model-pull, spec.md §9.1 step 4) await the
130
+ * SAME run rather than each starting their own (WP-78 fehlerklasse: the
131
+ * same in-flight-dedup requirement applies here, not just to `/api/embed`).
132
+ *
133
+ * The shared run itself is started with NO caller's `signal` (it is
134
+ * intentionally not tied to whichever caller happened to trigger it) —
135
+ * `raceWithSignal` below still lets THIS caller's own signal reject its
136
+ * own wait promptly, without cancelling the readiness sequence still
137
+ * running for other, still-waiting callers.
138
+ */
139
+ async ensureReadyOnce(signal) {
140
+ if (this.cachedReadiness) {
141
+ return;
142
+ }
143
+ if (!this.readyPromise) {
144
+ this.readyPromise = this.runReadiness().catch((error) => {
145
+ // A failed attempt must not be cached — the next call retries from scratch.
146
+ this.readyPromise = undefined;
147
+ throw error;
148
+ });
149
+ }
150
+ await raceWithSignal(this.readyPromise, signal);
151
+ }
152
+ async runReadiness() {
153
+ const outcome = await runReadinessSequence(this.config, {
154
+ httpClient: this.httpClient,
155
+ clock: this.clock,
156
+ random: this.random,
157
+ sleep: this.sleep,
158
+ backoffConfig: this.backoffConfig,
159
+ }, undefined);
160
+ this.cachedReadiness = outcome.readiness;
161
+ this.resolvedEndpointUrl = outcome.endpointUrl;
162
+ }
163
+ async embedTexts(texts, signal, onBatchProgress) {
164
+ const endpointUrl = this.resolvedEndpointUrl;
165
+ if (!endpointUrl) {
166
+ throw embeddingUnavailableError("Ollama embedding provider is not ready (no resolved endpoint).", {
167
+ retryable: false,
168
+ });
169
+ }
170
+ const slices = chunkIntoSlices(texts, Math.max(1, this.config.batchSize));
171
+ const batchCount = slices.length;
172
+ let embeddedCount = 0;
173
+ const sliceResults = await mapWithConcurrency(slices, this.config.maxConnections, async (slice, sliceIndex) => {
174
+ signal?.throwIfAborted();
175
+ const result = await this.embedSlice(slice, endpointUrl, signal);
176
+ embeddedCount += slice.length;
177
+ onBatchProgress?.({
178
+ batchIndex: sliceIndex,
179
+ batchCount,
180
+ embeddedCount,
181
+ totalCount: texts.length,
182
+ });
183
+ return result;
184
+ });
185
+ const vectors = sliceResults.flatMap((result) => result.vectors);
186
+ const truncated = sliceResults.flatMap((result) => result.truncated);
187
+ return { vectors, truncated };
188
+ }
189
+ async embedSlice(texts, endpointUrl, signal) {
190
+ const keys = texts.map((text) => ({
191
+ embeddingProfileId: this.profileId,
192
+ preparedTextHash: sha256Hex(text),
193
+ }));
194
+ const keyStrings = keys.map(cacheKeyString);
195
+ const cached = await Promise.all(keys.map((key) => this.cache.get(key, { signal })));
196
+ const vectors = new Array(texts.length);
197
+ const truncated = new Array(texts.length);
198
+ // Group the still-missing positions by cache key: (a) the SAME prepared
199
+ // text appearing more than once in this slice must be embedded only
200
+ // once (spec.md §9.4, WP-78 "Batch-interne Deduplizierung"), and (b) a
201
+ // key already being fetched by ANOTHER concurrent call is joined below
202
+ // instead of triggering a second `/api/embed` request.
203
+ const missGroups = new Map();
204
+ for (let index = 0; index < texts.length; index += 1) {
205
+ const hit = cached[index];
206
+ if (hit) {
207
+ vectors[index] = hit.vector;
208
+ truncated[index] = hit.truncated;
209
+ continue;
210
+ }
211
+ const keyString = keyStrings[index];
212
+ const group = missGroups.get(keyString);
213
+ if (group) {
214
+ group.push(index);
215
+ }
216
+ else {
217
+ missGroups.set(keyString, [index]);
218
+ }
219
+ }
220
+ if (missGroups.size === 0) {
221
+ return { vectors, truncated };
222
+ }
223
+ signal?.throwIfAborted();
224
+ // Claim/join in-flight fetches. This loop MUST NOT `await` anything: two
225
+ // concurrent `embedSlice` calls that both read a cache miss for the same
226
+ // key resume as two separate, atomically-run continuations (JS never
227
+ // interleaves them mid-loop), so whichever runs this claiming loop first
228
+ // wins the key and the other necessarily observes and joins it instead
229
+ // of starting its own fetch (WP-78 "Nebenläufigkeit").
230
+ const toFetch = [];
231
+ const pending = [];
232
+ for (const [keyString, indexes] of missGroups) {
233
+ const existing = this.inFlightEmbeds.get(keyString);
234
+ if (existing) {
235
+ pending.push({ indexes, promise: existing });
236
+ continue;
237
+ }
238
+ const deferred = createDeferred();
239
+ this.inFlightEmbeds.register(keyString, deferred.promise);
240
+ const firstIndex = indexes[0];
241
+ toFetch.push({
242
+ keyString,
243
+ key: keys[firstIndex],
244
+ text: texts[firstIndex],
245
+ deferred,
246
+ });
247
+ pending.push({ indexes, promise: deferred.promise });
248
+ }
249
+ if (toFetch.length > 0) {
250
+ // Deliberately not awaited here: the underlying request must survive
251
+ // (and keep serving every joined caller) even if THIS caller — the
252
+ // one that happened to claim the keys — is the one whose own signal
253
+ // aborts. Errors are routed to each `deferred` inside the method, so
254
+ // this can never become an unhandled rejection.
255
+ void this.fetchAndSettle(toFetch, endpointUrl);
256
+ }
257
+ await Promise.all(pending.map(async ({ indexes, promise }) => {
258
+ // `raceWithSignal`, not `promise` directly: THIS call's own signal
259
+ // must be able to reject THIS call without cancelling the shared
260
+ // fetch for any other caller still awaiting the same key.
261
+ const entry = await raceWithSignal(promise, signal);
262
+ for (const index of indexes) {
263
+ vectors[index] = entry.vector;
264
+ truncated[index] = entry.truncated;
265
+ }
266
+ }));
267
+ return { vectors, truncated };
268
+ }
269
+ /**
270
+ * Fetches every text in `items` in a SINGLE `/api/embed` request and
271
+ * settles each item's `deferred` with its own resulting cache entry, or
272
+ * rejects all of them on failure — which also frees their in-flight slots
273
+ * (`InFlightRegistry.register`'s cleanup), so the next call retries from
274
+ * scratch rather than being stuck on a broken promise (WP-78).
275
+ *
276
+ * Vor dem Absetzen des Requests wird der Cache ein ZWEITES Mal gelesen
277
+ * (WP-117, siehe {@link revalidateBeforeFetch}): die Miss-Entscheidung in
278
+ * `embedSlice` kann veraltet sein, sobald `cache.get()` verzögert auflöst.
279
+ *
280
+ * Intentionally takes no `AbortSignal`: this operation is shared by
281
+ * however many callers joined it, so it is not tied to any single one of
282
+ * their lifecycles (see `embedSlice`/`raceWithSignal`).
283
+ */
284
+ async fetchAndSettle(items, endpointUrl) {
285
+ try {
286
+ // WP-117: erst jetzt, mit den bereits beanspruchten In-flight-Plätzen
287
+ // in der Hand, steht fest, was wirklich über die Leitung muss.
288
+ const outstanding = await this.revalidateBeforeFetch(items);
289
+ if (outstanding.length === 0) {
290
+ return;
291
+ }
292
+ const texts = outstanding.map((item) => item.text);
293
+ const response = await withRetry(() => embed(this.httpClient, endpointUrl, this.profile.model, texts), this.backoffConfig, { random: this.random, sleep: this.sleep }, { isRetryable: isRetryableEmbedError });
294
+ if (response.embeddings.length !== outstanding.length) {
295
+ throw embeddingUnavailableError(`Ollama /api/embed returned ${response.embeddings.length} vectors for ${outstanding.length} inputs.`, { retryable: false });
296
+ }
297
+ for (let j = 0; j < outstanding.length; j += 1) {
298
+ const item = outstanding[j];
299
+ const vector = response.embeddings[j];
300
+ assertVectorDimension(vector, this.profile.dimensions, {
301
+ endpoint: endpointUrl,
302
+ model: this.profile.model,
303
+ stage: "embed",
304
+ });
305
+ const isTruncated = wouldProviderTruncate(item.text, this.config.contextLength);
306
+ const entry = { vector, truncated: isTruncated };
307
+ await this.cache.set(item.key, entry);
308
+ item.deferred.resolve(entry);
309
+ }
310
+ }
311
+ catch (error) {
312
+ // Bewusst über `items` und nicht über `outstanding`: schlägt schon die
313
+ // Revalidierung fehl, ist `outstanding` gar nicht bestimmt. Ein bereits
314
+ // aus dem Cache aufgelöstes Deferred ignoriert das spätere `reject`
315
+ // (die erste Festlegung gewinnt), ein noch offenes wird sicher
316
+ // abgewiesen — kein Aufrufer bleibt hängen.
317
+ for (const item of items) {
318
+ item.deferred.reject(error);
319
+ }
320
+ }
321
+ }
322
+ /**
323
+ * Zweite Cache-Prüfung unmittelbar vor dem Request (WP-117).
324
+ *
325
+ * `embedSlice` trifft die Miss-/Join-Entscheidung EINMAL, aus einem
326
+ * einzigen `Promise.all` über `cache.get()`. Ein Cache, dessen `get()` den
327
+ * Zustand zum Aufrufzeitpunkt liest, aber erst später auflöst — also jeder
328
+ * netzgebundene Cache und damit genau der vorgesehene Erweiterungspunkt
329
+ * für einen persistenten Adapter —, öffnet ein Fenster: Aufrufer A kann
330
+ * zwischen der Anfrage und dem Auflösen von B's `get()` vollständig fertig
331
+ * werden, seinen Eintrag schreiben und seinen In-flight-Platz wieder
332
+ * freigeben. B fände dann weder Cache-Treffer noch In-flight-Eintrag und
333
+ * würde denselben Text ein zweites Mal einbetten.
334
+ *
335
+ * Diese Prüfung schließt das Fenster, weil sie STRIKT NACH der Beanspruchung
336
+ * des In-flight-Platzes liegt: der Platz wird erst freigegeben, nachdem
337
+ * `cache.set()` aufgelöst hat (siehe unten), also sieht ein `get()`, das
338
+ * danach abgesetzt wird, den geschriebenen Eintrag.
339
+ *
340
+ * Kein Deadlock und keine Wiederholungsschleife: genau EIN zusätzliches
341
+ * `cache.get()` je beanspruchtem Schlüssel, kein Warten auf fremde
342
+ * Deferreds (die eigenen Plätze hält dieser Aufruf selbst, ein anderer
343
+ * Aufrufer kann sie also nicht beanspruchen), und jedes Deferred wird auf
344
+ * jedem Pfad festgelegt — aus dem Cache, aus der Antwort oder per
345
+ * `reject` im `catch`.
346
+ *
347
+ * Kein `AbortSignal`: dieser Lauf gehört keinem einzelnen Aufrufer
348
+ * (dieselbe Begründung wie bei {@link fetchAndSettle}).
349
+ *
350
+ * Ein Fehler des Caches wird NICHT verschluckt (E4), sondern nach oben
351
+ * gereicht und weist damit die beteiligten Deferreds ab — dasselbe
352
+ * sichtbare Verhalten wie bei einem Fehlschlag der ERSTEN Cache-Lesung in
353
+ * `embedSlice`. Preis dieser Prüfung: eine zusätzliche Cache-Lesung je
354
+ * echtem Miss (dokumentiert in der README).
355
+ */
356
+ async revalidateBeforeFetch(items) {
357
+ // Erst alle Lesungen abschließen, dann festlegen: so ist bei einem Fehler
358
+ // noch kein Deferred aufgelöst und der `catch` in `fetchAndSettle` weist
359
+ // wirklich alle ab.
360
+ const lateHits = await Promise.all(items.map(async (item) => this.cache.get(item.key)));
361
+ const outstanding = [];
362
+ for (let index = 0; index < items.length; index += 1) {
363
+ const item = items[index];
364
+ const hit = lateHits[index];
365
+ if (hit) {
366
+ item.deferred.resolve(hit);
367
+ continue;
368
+ }
369
+ outstanding.push(item);
370
+ }
371
+ return outstanding;
372
+ }
373
+ }
374
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Readiness sequence (spec.md §9.1), run exactly in the spec's order:
3
+ *
4
+ * 1. determine the effective endpoint;
5
+ * 2. check reachability;
6
+ * 3. check model availability;
7
+ * 4. pull the model in managed mode if needed;
8
+ * 5. produce a probe embedding;
9
+ * 6. validate the vector dimension;
10
+ * 7. cache profile and probe timestamp (done by the caller, `provider.ts`,
11
+ * which owns the cached `EmbeddingReadinessV1`).
12
+ */
13
+ import type { Clock, EmbeddingReadinessV1, Random } from "@untangle-code/domain";
14
+ import type { BackoffConfig } from "./backoff.js";
15
+ import { type OllamaHttpClient } from "./http.js";
16
+ import type { SleepFn, OllamaEmbeddingProviderConfig } from "./types.js";
17
+ export declare const READINESS_PROBE_TEXT = "untangle embedding readiness probe";
18
+ export interface ReadinessDeps {
19
+ httpClient: OllamaHttpClient;
20
+ clock: Clock;
21
+ random: Random;
22
+ sleep: SleepFn;
23
+ backoffConfig: BackoffConfig;
24
+ }
25
+ export interface ReadinessOutcome {
26
+ readiness: EmbeddingReadinessV1;
27
+ /** The resolved, non-redacted endpoint — used internally for subsequent embed calls. */
28
+ endpointUrl: string;
29
+ }
30
+ export declare function runReadinessSequence(config: OllamaEmbeddingProviderConfig, deps: ReadinessDeps, signal?: AbortSignal): Promise<ReadinessOutcome>;
31
+ //# sourceMappingURL=readiness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readiness.d.ts","sourceRoot":"","sources":["../src/readiness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAGjF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGlD,OAAO,EAA0C,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAQ1F,OAAO,KAAK,EAAE,OAAO,EAAE,6BAA6B,EAAE,MAAM,YAAY,CAAC;AAIzE,eAAO,MAAM,oBAAoB,uCAAuC,CAAC;AAEzE,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,gBAAgB,CAAC;IAC7B,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,oBAAoB,CAAC;IAChC,wFAAwF;IACxF,WAAW,EAAE,MAAM,CAAC;CACrB;AA6FD,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,6BAA6B,EACrC,IAAI,EAAE,aAAa,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,gBAAgB,CAAC,CA6D3B"}
@@ -0,0 +1,105 @@
1
+ import { configInvalidError, embeddingUnavailableError } from "@untangle-code/observability";
2
+ import { withRetry } from "./backoff.js";
3
+ import { redactEndpointUrl } from "./endpoint.js";
4
+ import { isRequestCancellation, OllamaHttpError } from "./http.js";
5
+ import { embed, listTags, pullModel, tagsContainModel, } from "./ollama-api.js";
6
+ import { DEFAULT_MANAGED_URL, DEFAULT_NATIVE_PROBE_URL } from "./types.js";
7
+ import { assertVectorDimension } from "./validation.js";
8
+ export const READINESS_PROBE_TEXT = "untangle embedding readiness probe";
9
+ function isRetryableOllamaError(error) {
10
+ return error instanceof OllamaHttpError && error.retryable;
11
+ }
12
+ async function requestWithRetry(fn, deps, signal) {
13
+ return withRetry(fn, deps.backoffConfig, { random: deps.random, sleep: deps.sleep }, {
14
+ signal,
15
+ isRetryable: isRetryableOllamaError,
16
+ });
17
+ }
18
+ /** `GET /api/tags`, treating any failure as "not reachable" — EXCEPT cancellation, which propagates. */
19
+ async function tryListTags(url, deps, signal) {
20
+ try {
21
+ return await requestWithRetry(() => listTags(deps.httpClient, url, signal), deps, signal);
22
+ }
23
+ catch (error) {
24
+ if (isRequestCancellation(error, signal)) {
25
+ throw error;
26
+ }
27
+ return null;
28
+ }
29
+ }
30
+ async function checkReachable(url, deps, signal) {
31
+ const tags = await tryListTags(url, deps, signal);
32
+ if (!tags) {
33
+ throw embeddingUnavailableError(`Ollama endpoint ${redactEndpointUrl(url)} is not reachable.`, {
34
+ retryable: true,
35
+ context: { endpoint: redactEndpointUrl(url) },
36
+ });
37
+ }
38
+ return tags;
39
+ }
40
+ /** spec.md §9.1 steps 1-2: determine the effective endpoint and confirm it is reachable. */
41
+ async function resolveEndpoint(config, deps, signal) {
42
+ if (config.mode === "external") {
43
+ if (!config.url) {
44
+ throw configInvalidError('EmbeddingConfig.url is required when embeddings.mode is "external" (spec.md §4.5).', { retryable: false });
45
+ }
46
+ return {
47
+ url: config.url,
48
+ managed: false,
49
+ tags: await checkReachable(config.url, deps, signal),
50
+ };
51
+ }
52
+ if (config.mode === "managed") {
53
+ const url = config.url ?? config.managedUrl ?? DEFAULT_MANAGED_URL;
54
+ return { url, managed: true, tags: await checkReachable(url, deps, signal) };
55
+ }
56
+ // "auto" (spec.md §3.1): native probe first, managed endpoint as fallback.
57
+ const nativeUrl = config.nativeProbeUrl ?? DEFAULT_NATIVE_PROBE_URL;
58
+ const nativeTags = await tryListTags(nativeUrl, deps, signal);
59
+ if (nativeTags) {
60
+ return { url: nativeUrl, managed: false, tags: nativeTags };
61
+ }
62
+ const managedUrl = config.url ?? config.managedUrl ?? DEFAULT_MANAGED_URL;
63
+ return { url: managedUrl, managed: true, tags: await checkReachable(managedUrl, deps, signal) };
64
+ }
65
+ export async function runReadinessSequence(config, deps, signal) {
66
+ const { url, managed, tags } = await resolveEndpoint(config, deps, signal);
67
+ const modelName = config.profile.model;
68
+ // Step 3: model availability.
69
+ let available = tagsContainModel(tags.models, modelName);
70
+ // Step 4: pull in managed mode if needed (spec.md §3.2 forbids touching an external endpoint).
71
+ if (!available) {
72
+ if (!managed) {
73
+ throw embeddingUnavailableError(`Ollama model "${modelName}" is not available at ${redactEndpointUrl(url)}, and this endpoint is not managed by Untangle — pull it manually (spec.md §3.2).`, { retryable: false, context: { endpoint: redactEndpointUrl(url), model: modelName } });
74
+ }
75
+ await requestWithRetry(() => pullModel(deps.httpClient, url, modelName, signal), deps, signal);
76
+ const refreshed = await requestWithRetry(() => listTags(deps.httpClient, url, signal), deps, signal);
77
+ available = tagsContainModel(refreshed.models, modelName);
78
+ if (!available) {
79
+ throw embeddingUnavailableError(`Ollama model "${modelName}" was still not available after pulling it from ${redactEndpointUrl(url)}.`, { retryable: false, context: { endpoint: redactEndpointUrl(url), model: modelName } });
80
+ }
81
+ }
82
+ // Step 5: probe embedding.
83
+ const probeResponse = await requestWithRetry(() => embed(deps.httpClient, url, modelName, [READINESS_PROBE_TEXT], signal), deps, signal);
84
+ const probeVector = probeResponse.embeddings[0];
85
+ if (!probeVector) {
86
+ throw embeddingUnavailableError(`Ollama probe embedding returned no vector for model "${modelName}".`, { retryable: false, context: { endpoint: redactEndpointUrl(url), model: modelName } });
87
+ }
88
+ // Step 6: dimension validation (must fail BEFORE the first upsert, spec.md §4.8).
89
+ assertVectorDimension(probeVector, config.profile.dimensions, {
90
+ endpoint: redactEndpointUrl(url),
91
+ model: modelName,
92
+ stage: "readiness_probe",
93
+ });
94
+ // Step 7: profile + probe timestamp are cached by the caller from this result.
95
+ return {
96
+ readiness: {
97
+ endpoint: redactEndpointUrl(url),
98
+ model: modelName,
99
+ dimensions: probeVector.length,
100
+ probedAt: deps.clock.nowIso(),
101
+ },
102
+ endpointUrl: url,
103
+ };
104
+ }
105
+ //# sourceMappingURL=readiness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readiness.js","sourceRoot":"","sources":["../src/readiness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Real, abortable {@link SleepFn} implementation. The only production
3
+ * implementation of the port declared in `types.ts`; tests inject a fake
4
+ * that resolves immediately instead of waiting for real time to pass.
5
+ */
6
+ import type { SleepFn } from "./types.js";
7
+ export declare const realSleep: SleepFn;
8
+ //# sourceMappingURL=sleep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sleep.d.ts","sourceRoot":"","sources":["../src/sleep.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,eAAO,MAAM,SAAS,EAAE,OAmBpB,CAAC"}
package/dist/sleep.js ADDED
@@ -0,0 +1,20 @@
1
+ export const realSleep = (ms, signal) => new Promise((resolve, reject) => {
2
+ if (signal?.aborted) {
3
+ reject(signal.reason);
4
+ return;
5
+ }
6
+ const timer = setTimeout(() => {
7
+ cleanup();
8
+ resolve();
9
+ }, ms);
10
+ const onAbort = () => {
11
+ cleanup();
12
+ reject(signal?.reason);
13
+ };
14
+ function cleanup() {
15
+ clearTimeout(timer);
16
+ signal?.removeEventListener("abort", onAbort);
17
+ }
18
+ signal?.addEventListener("abort", onAbort, { once: true });
19
+ });
20
+ //# sourceMappingURL=sleep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sleep.js","sourceRoot":"","sources":["../src/sleep.ts"],"names":[],"mappings":"AAOA,MAAM,CAAC,MAAM,SAAS,GAAY,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAC/C,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IACpC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;QACpB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACtB,OAAO;IACT,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;QAC5B,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;IACZ,CAAC,EAAE,EAAE,CAAC,CAAC;IACP,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,OAAO,EAAE,CAAC;QACV,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzB,CAAC,CAAC;IACF,SAAS,OAAO;QACd,YAAY,CAAC,KAAK,CAAC,CAAC;QACpB,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAChD,CAAC;IACD,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AAC7D,CAAC,CAAC,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Truncation guard (spec.md §9.3).
3
+ *
4
+ * Ollama's `/api/embed` truncates silently to the model's context window by
5
+ * default (`truncate: true`) and does not report, per input, whether
6
+ * truncation actually happened. The chunker (a separate package) is already
7
+ * responsible for keeping prepared text within budget in the normal case;
8
+ * this guard exists purely to catch the exceptional case where it did not
9
+ * and flag the resulting point honestly rather than silently accepting a
10
+ * vector computed from a truncated text (vision.md 8.4 "no silent data
11
+ * loss").
12
+ *
13
+ * Detection is a conservative, offline token estimate compared against the
14
+ * configured `contextLength` — the same trade-off `@untangle-code/chunker`
15
+ * makes for its own budget checks (over-estimating real BPE token counts is
16
+ * far safer than under-estimating, which would hide a real truncation).
17
+ * The estimator is intentionally duplicated rather than imported: chunker
18
+ * uses it to fit a budget ahead of time, this module uses it to flag a
19
+ * budget miss after the fact, and importing chunker here would add a
20
+ * runtime dependency between packages that otherwise have no reason to
21
+ * know about each other (rule 3 spirit).
22
+ */
23
+ /** Estimates a conservative (over-)approximation of `text`'s token count. */
24
+ export declare function estimateTokenCount(text: string): number;
25
+ /**
26
+ * True when `text`'s estimated token count exceeds `contextLength` — the
27
+ * point at which Ollama's provider-side truncation actually applies.
28
+ */
29
+ export declare function wouldProviderTruncate(text: string, contextLength: number): boolean;
30
+ //# sourceMappingURL=truncation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"truncation.d.ts","sourceRoot":"","sources":["../src/truncation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKvD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAElF"}