simple-dynamsoft-mcp 6.3.0 → 7.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.
Files changed (47) hide show
  1. package/.env.example +35 -9
  2. package/README.md +156 -497
  3. package/package.json +13 -7
  4. package/scripts/prebuild-rag-index.mjs +1 -1
  5. package/scripts/run-gemini-tests.mjs +1 -1
  6. package/scripts/sync-submodules.mjs +1 -1
  7. package/scripts/verify-doc-resources.mjs +79 -0
  8. package/src/data/bootstrap.js +475 -0
  9. package/src/data/download-utils.js +99 -0
  10. package/src/data/hydration-mode.js +15 -0
  11. package/src/data/hydration-policy.js +39 -0
  12. package/src/data/repo-map.js +149 -0
  13. package/src/{data-root.js → data/root.js} +1 -1
  14. package/src/{submodule-sync.js → data/submodule-sync.js} +1 -1
  15. package/src/index.js +49 -1499
  16. package/src/observability/logging.js +51 -0
  17. package/src/rag/config.js +96 -0
  18. package/src/rag/index.js +266 -0
  19. package/src/rag/lexical-provider.js +170 -0
  20. package/src/rag/logger.js +46 -0
  21. package/src/rag/profile-config.js +48 -0
  22. package/src/rag/providers.js +585 -0
  23. package/src/rag/search-utils.js +166 -0
  24. package/src/rag/vector-cache.js +323 -0
  25. package/src/server/create-server.js +168 -0
  26. package/src/server/helpers/server-helpers.js +33 -0
  27. package/src/{resource-index → server/resource-index}/paths.js +2 -2
  28. package/src/{resource-index → server/resource-index}/samples.js +9 -1
  29. package/src/{resource-index.js → server/resource-index.js} +158 -93
  30. package/src/server/resources/register-resources.js +56 -0
  31. package/src/server/runtime-config.js +66 -0
  32. package/src/server/tools/register-index-tools.js +130 -0
  33. package/src/server/tools/register-project-tools.js +305 -0
  34. package/src/server/tools/register-quickstart-tools.js +572 -0
  35. package/src/server/tools/register-sample-tools.js +333 -0
  36. package/src/server/tools/register-version-tools.js +136 -0
  37. package/src/server/transports/http.js +84 -0
  38. package/src/server/transports/stdio.js +12 -0
  39. package/src/data-bootstrap.js +0 -255
  40. package/src/rag.js +0 -1203
  41. /package/src/{gemini-retry.js → rag/gemini-retry.js} +0 -0
  42. /package/src/{normalizers.js → server/normalizers.js} +0 -0
  43. /package/src/{resource-index → server/resource-index}/builders.js +0 -0
  44. /package/src/{resource-index → server/resource-index}/config.js +0 -0
  45. /package/src/{resource-index → server/resource-index}/docs-loader.js +0 -0
  46. /package/src/{resource-index → server/resource-index}/uri.js +0 -0
  47. /package/src/{resource-index → server/resource-index}/version-policy.js +0 -0
@@ -0,0 +1,585 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import {
5
+ sleepMs,
6
+ parseRetryAfterMs,
7
+ normalizeGeminiRetryConfig,
8
+ isRateLimitGeminiStatus,
9
+ GeminiHttpError,
10
+ executeWithGeminiRetry
11
+ } from "./gemini-retry.js";
12
+
13
+ function ensureDirectory(path) {
14
+ if (!existsSync(path)) {
15
+ mkdirSync(path, { recursive: true });
16
+ }
17
+ }
18
+
19
+ function resolveProviderChain(ragConfig) {
20
+ let primary = ragConfig.provider;
21
+ if (primary === "auto") {
22
+ primary = ragConfig.geminiApiKey ? "gemini" : "local";
23
+ }
24
+ const chain = [primary];
25
+ if (ragConfig.fallback && ragConfig.fallback !== "none" && ragConfig.fallback !== primary) {
26
+ chain.push(ragConfig.fallback);
27
+ }
28
+ return Array.from(new Set(chain));
29
+ }
30
+
31
+ async function embedTextsWithProgress(
32
+ texts,
33
+ embedder,
34
+ batchSize = 1,
35
+ {
36
+ offset = 0,
37
+ total = texts.length,
38
+ onChunk = null,
39
+ providerName = "",
40
+ logRag,
41
+ isRateLimitError
42
+ } = {}
43
+ ) {
44
+ const results = [];
45
+ const normalizedBatchSize = Math.max(1, batchSize);
46
+ let completed = offset;
47
+ let currentBatchSize = normalizedBatchSize;
48
+ let rateLimitFailures = 0;
49
+ let batchDowngrades = 0;
50
+ let singleFallbackBatches = 0;
51
+
52
+ const reportChunk = async (vectors, mode, sourceBatchSize) => {
53
+ if (!Array.isArray(vectors) || vectors.length === 0) return;
54
+ completed += vectors.length;
55
+ if (onChunk) {
56
+ await onChunk({
57
+ vectors,
58
+ mode,
59
+ sourceBatchSize,
60
+ completed,
61
+ total
62
+ });
63
+ }
64
+ };
65
+
66
+ if (embedder.embedBatch && normalizedBatchSize > 1) {
67
+ let index = 0;
68
+ while (index < texts.length) {
69
+ const batch = texts.slice(index, index + currentBatchSize);
70
+ try {
71
+ const vectors = await embedder.embedBatch(batch);
72
+ if (!Array.isArray(vectors) || vectors.length !== batch.length) {
73
+ throw new Error(`Gemini batch response size mismatch expected=${batch.length} actual=${vectors?.length || 0}`);
74
+ }
75
+ results.push(...vectors);
76
+ index += batch.length;
77
+ rateLimitFailures = 0;
78
+ await reportChunk(vectors, "batch", batch.length);
79
+ } catch (error) {
80
+ if (isRateLimitError(error)) {
81
+ rateLimitFailures += 1;
82
+ const nextBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
83
+ if (nextBatchSize < currentBatchSize) {
84
+ batchDowngrades += 1;
85
+ logRag(
86
+ `gemini batch downgrade provider=${providerName || "unknown"} from=${currentBatchSize} to=${nextBatchSize} ` +
87
+ `rate_limit_failures=${rateLimitFailures}`
88
+ );
89
+ currentBatchSize = nextBatchSize;
90
+ continue;
91
+ }
92
+ }
93
+
94
+ singleFallbackBatches += 1;
95
+ logRag(
96
+ `batch embedding fallback provider=${providerName || "unknown"} batch_size=${batch.length} reason=${error.message}`
97
+ );
98
+ for (const text of batch) {
99
+ const vector = await embedder.embed(text);
100
+ results.push(vector);
101
+ await reportChunk([vector], "single_fallback", 1);
102
+ }
103
+ index += batch.length;
104
+ rateLimitFailures = 0;
105
+ }
106
+ }
107
+
108
+ return {
109
+ vectors: results,
110
+ stats: {
111
+ batchDowngrades,
112
+ singleFallbackBatches,
113
+ finalBatchSize: currentBatchSize
114
+ }
115
+ };
116
+ }
117
+
118
+ for (const text of texts) {
119
+ const vector = await embedder.embed(text);
120
+ results.push(vector);
121
+ await reportChunk([vector], "single", 1);
122
+ }
123
+
124
+ return {
125
+ vectors: results,
126
+ stats: {
127
+ batchDowngrades,
128
+ singleFallbackBatches,
129
+ finalBatchSize: 1
130
+ }
131
+ };
132
+ }
133
+
134
+ function createProviderOrchestrator({
135
+ pkgVersion,
136
+ ragConfig,
137
+ ragLogState,
138
+ logRag,
139
+ resourceIndex,
140
+ resourceIndexByUri,
141
+ createLexicalProvider,
142
+ getRagSignatureData,
143
+ utils,
144
+ vectorCache
145
+ }) {
146
+ let fuseSearch = utils.createFuseSearch(resourceIndex);
147
+ const providerCache = new Map();
148
+ let localEmbedderPromise = null;
149
+ let geminiEmbedderPromise = null;
150
+
151
+ async function getLocalEmbedder() {
152
+ if (localEmbedderPromise) return localEmbedderPromise;
153
+ localEmbedderPromise = (async () => {
154
+ const { pipeline, env } = await import("@xenova/transformers");
155
+ ensureDirectory(ragConfig.modelCacheDir);
156
+ if (!ragLogState.localEmbedderInit) {
157
+ ragLogState.localEmbedderInit = true;
158
+ logRag(
159
+ `init local embedder model=${ragConfig.localModel} quantized=${ragConfig.localQuantized} model_cache_dir=${ragConfig.modelCacheDir}`
160
+ );
161
+ }
162
+ env.cacheDir = ragConfig.modelCacheDir;
163
+ env.allowLocalModels = true;
164
+ const extractor = await pipeline("feature-extraction", ragConfig.localModel, {
165
+ quantized: ragConfig.localQuantized
166
+ });
167
+ return {
168
+ embed: async (text) => {
169
+ const output = await extractor(text, { pooling: "mean", normalize: true });
170
+ return Array.from(output.data);
171
+ }
172
+ };
173
+ })();
174
+ return localEmbedderPromise;
175
+ }
176
+
177
+ async function getGeminiEmbedder() {
178
+ if (!ragConfig.geminiApiKey) {
179
+ throw new Error("GEMINI_API_KEY is required for gemini embeddings.");
180
+ }
181
+ if (geminiEmbedderPromise) return geminiEmbedderPromise;
182
+ const retryConfig = normalizeGeminiRetryConfig({
183
+ maxAttempts: ragConfig.geminiRetryMaxAttempts,
184
+ baseDelayMs: ragConfig.geminiRetryBaseDelayMs,
185
+ maxDelayMs: ragConfig.geminiRetryMaxDelayMs,
186
+ requestThrottleMs: ragConfig.geminiRequestThrottleMs
187
+ });
188
+
189
+ geminiEmbedderPromise = Promise.resolve((() => {
190
+ const metrics = {
191
+ requests: 0,
192
+ retries: 0,
193
+ retryDelayMs: 0,
194
+ throttleEvents: 0,
195
+ throttleDelayMs: 0,
196
+ rateLimitRetries: 0
197
+ };
198
+
199
+ let nextAllowedAt = 0;
200
+
201
+ const throttleRequest = async (operation) => {
202
+ if (retryConfig.requestThrottleMs <= 0) return;
203
+ const now = Date.now();
204
+ const waitMs = Math.max(0, nextAllowedAt - now);
205
+ if (waitMs > 0) {
206
+ metrics.throttleEvents += 1;
207
+ metrics.throttleDelayMs += waitMs;
208
+ logRag(`gemini throttle op=${operation} wait_ms=${waitMs}`);
209
+ await sleepMs(waitMs);
210
+ }
211
+ nextAllowedAt = Date.now() + retryConfig.requestThrottleMs;
212
+ };
213
+
214
+ const requestJson = async (operation, endpoint, body) => executeWithGeminiRetry({
215
+ operation,
216
+ retryConfig,
217
+ logger: (message) => logRag(message),
218
+ onRetry: ({ delayMs, rateLimited }) => {
219
+ metrics.retries += 1;
220
+ metrics.retryDelayMs += delayMs;
221
+ if (rateLimited) {
222
+ metrics.rateLimitRetries += 1;
223
+ }
224
+ },
225
+ requestFn: async () => {
226
+ await throttleRequest(operation);
227
+ metrics.requests += 1;
228
+ const response = await fetch(
229
+ `${ragConfig.geminiBaseUrl}/v1beta/${endpoint}?key=${ragConfig.geminiApiKey}`,
230
+ {
231
+ method: "POST",
232
+ headers: { "Content-Type": "application/json" },
233
+ body: JSON.stringify(body)
234
+ }
235
+ );
236
+ if (!response.ok) {
237
+ const detail = await response.text();
238
+ throw new GeminiHttpError(`Gemini ${operation} failed (${response.status}): ${detail}`, {
239
+ status: response.status,
240
+ detail,
241
+ retryAfterMs: parseRetryAfterMs(response.headers.get("retry-after"))
242
+ });
243
+ }
244
+ return response.json();
245
+ }
246
+ });
247
+
248
+ return {
249
+ embed: async (text) => {
250
+ const payload = await requestJson(
251
+ "embedContent",
252
+ `${ragConfig.geminiModel}:embedContent`,
253
+ {
254
+ content: {
255
+ parts: [{ text }]
256
+ }
257
+ }
258
+ );
259
+ const embedding = payload.embedding?.values || payload.embedding || payload.embeddings?.[0]?.values;
260
+ if (!embedding) {
261
+ throw new Error("Gemini embedding response missing embedding values.");
262
+ }
263
+ return embedding;
264
+ },
265
+ embedBatch: async (texts) => {
266
+ const payload = await requestJson(
267
+ "batchEmbedContents",
268
+ `${ragConfig.geminiModel}:batchEmbedContents`,
269
+ {
270
+ requests: texts.map((text) => ({
271
+ model: ragConfig.geminiModel,
272
+ content: {
273
+ parts: [{ text }]
274
+ }
275
+ }))
276
+ }
277
+ );
278
+ const embeddings = payload.embeddings || payload.responses;
279
+ if (!Array.isArray(embeddings)) {
280
+ throw new Error("Gemini batch response missing embeddings.");
281
+ }
282
+ return embeddings.map((item) => item.values || item.embedding?.values || item.embedding);
283
+ },
284
+ getMetrics: () => ({ ...metrics }),
285
+ resetMetrics: () => {
286
+ metrics.requests = 0;
287
+ metrics.retries = 0;
288
+ metrics.retryDelayMs = 0;
289
+ metrics.throttleEvents = 0;
290
+ metrics.throttleDelayMs = 0;
291
+ metrics.rateLimitRetries = 0;
292
+ }
293
+ };
294
+ })());
295
+ return geminiEmbedderPromise;
296
+ }
297
+
298
+ async function createVectorProvider({ name, model, embedder, batchSize }) {
299
+ const signature = utils.buildIndexSignature({
300
+ pkgVersion,
301
+ signatureData: getRagSignatureData(),
302
+ ragConfig
303
+ });
304
+ const cacheMeta = {
305
+ provider: name,
306
+ model,
307
+ signature
308
+ };
309
+ const cacheKey = createHash("sha256").update(JSON.stringify(cacheMeta)).digest("hex");
310
+ const cacheFile = join(ragConfig.cacheDir, vectorCache.makeCacheFileName(name, model, cacheKey));
311
+ const checkpointFile = join(ragConfig.cacheDir, vectorCache.makeCheckpointFileName(name, model, cacheKey));
312
+ const expectedCacheState = {
313
+ cacheKey,
314
+ signature,
315
+ provider: name,
316
+ model
317
+ };
318
+ logRag(
319
+ `provider=${name} cache_file=${cacheFile} rebuild=${ragConfig.rebuild} cache_key=${cacheKey.slice(0, 12)}`
320
+ );
321
+
322
+ let indexPromise = null;
323
+ const loadIndex = async () => {
324
+ if (indexPromise) return indexPromise;
325
+ indexPromise = (async () => {
326
+ if (!ragConfig.rebuild) {
327
+ let cacheState = vectorCache.loadVectorIndexCache(cacheFile, expectedCacheState);
328
+ if (cacheState.hit) {
329
+ const cached = cacheState.payload;
330
+ logRag(
331
+ `cache hit provider=${name} file=${cacheFile} items=${cached.items.length} vectors=${cached.vectors.length}`
332
+ );
333
+ return {
334
+ items: cached.items,
335
+ vectors: cached.vectors
336
+ };
337
+ }
338
+ logRag(`cache miss provider=${name} file=${cacheFile} reason=${cacheState.reason}`);
339
+
340
+ const downloadResult = await vectorCache.maybeDownloadPrebuiltVectorIndex({
341
+ provider: name,
342
+ model,
343
+ cacheKey,
344
+ signature,
345
+ cacheFile
346
+ });
347
+ if (downloadResult.downloaded) {
348
+ cacheState = vectorCache.loadVectorIndexCache(cacheFile, expectedCacheState);
349
+ if (cacheState.hit) {
350
+ const cached = cacheState.payload;
351
+ logRag(
352
+ `cache hit provider=${name} file=${cacheFile} source=prebuilt_download items=${cached.items.length} vectors=${cached.vectors.length}`
353
+ );
354
+ return {
355
+ items: cached.items,
356
+ vectors: cached.vectors
357
+ };
358
+ }
359
+ logRag(`cache miss provider=${name} file=${cacheFile} source=prebuilt_download reason=${cacheState.reason}`);
360
+ }
361
+ } else {
362
+ logRag(`cache bypass provider=${name} file=${cacheFile} reason=rebuild_true`);
363
+ vectorCache.clearVectorIndexCheckpoint(checkpointFile);
364
+ }
365
+
366
+ const items = utils.buildEmbeddingItems(resourceIndex, ragConfig);
367
+ const texts = items.map((item) => item.text);
368
+ const indexedItems = items.map((item) => ({ id: item.id, uri: item.uri }));
369
+ let normalized = [];
370
+ let resumeFrom = 0;
371
+ if (!ragConfig.rebuild) {
372
+ const checkpointState = vectorCache.loadVectorIndexCheckpoint(checkpointFile, cacheKey, indexedItems);
373
+ if (checkpointState.hit) {
374
+ normalized = checkpointState.payload.vectors;
375
+ resumeFrom = normalized.length;
376
+ logRag(
377
+ `checkpoint resume provider=${name} file=${checkpointFile} completed=${resumeFrom}/${texts.length}`
378
+ );
379
+ } else if (checkpointState.reason !== "missing") {
380
+ logRag(`checkpoint ignored provider=${name} file=${checkpointFile} reason=${checkpointState.reason}`);
381
+ }
382
+ }
383
+
384
+ if (name === "gemini" && embedder.resetMetrics) {
385
+ embedder.resetMetrics();
386
+ }
387
+
388
+ const checkpointIntervalMs = 5000;
389
+ let lastCheckpointAt = 0;
390
+ const persistCheckpoint = (force = false) => {
391
+ const now = Date.now();
392
+ if (!force && now - lastCheckpointAt < checkpointIntervalMs) return;
393
+ const payload = {
394
+ cacheKey,
395
+ meta: cacheMeta,
396
+ items: indexedItems,
397
+ vectors: normalized,
398
+ completed: normalized.length,
399
+ total: texts.length,
400
+ updatedAt: new Date().toISOString()
401
+ };
402
+ vectorCache.saveVectorIndexCheckpoint(checkpointFile, payload);
403
+ lastCheckpointAt = now;
404
+ };
405
+
406
+ if (resumeFrom < texts.length) {
407
+ logRag(
408
+ `building index provider=${name} embed_items=${texts.length} remaining=${texts.length - resumeFrom} batch_size=${batchSize}`
409
+ );
410
+ try {
411
+ const embeddingResult = await embedTextsWithProgress(
412
+ texts.slice(resumeFrom),
413
+ embedder,
414
+ batchSize,
415
+ {
416
+ offset: resumeFrom,
417
+ total: texts.length,
418
+ providerName: name,
419
+ logRag,
420
+ isRateLimitError: (error) => utils.isRateLimitError(error, isRateLimitGeminiStatus),
421
+ onChunk: ({ vectors, completed, total }) => {
422
+ normalized.push(...vectors.map(utils.normalizeVector));
423
+ persistCheckpoint(completed >= total);
424
+ }
425
+ }
426
+ );
427
+
428
+ if (name === "gemini") {
429
+ const metrics = embedder.getMetrics ? embedder.getMetrics() : {};
430
+ logRag(
431
+ `gemini build metrics provider=${name} requests=${metrics.requests || 0} retries=${metrics.retries || 0} ` +
432
+ `retry_delay_ms=${metrics.retryDelayMs || 0} throttle_events=${metrics.throttleEvents || 0} ` +
433
+ `throttle_delay_ms=${metrics.throttleDelayMs || 0} rate_limit_retries=${metrics.rateLimitRetries || 0} ` +
434
+ `batch_downgrades=${embeddingResult.stats.batchDowngrades} single_fallback_batches=${embeddingResult.stats.singleFallbackBatches} ` +
435
+ `final_batch_size=${embeddingResult.stats.finalBatchSize}`
436
+ );
437
+ }
438
+ } catch (error) {
439
+ persistCheckpoint(true);
440
+ if (name === "gemini") {
441
+ const metrics = embedder.getMetrics ? embedder.getMetrics() : {};
442
+ logRag(
443
+ `gemini build failed provider=${name} requests=${metrics.requests || 0} retries=${metrics.retries || 0} ` +
444
+ `retry_delay_ms=${metrics.retryDelayMs || 0} throttle_events=${metrics.throttleEvents || 0} ` +
445
+ `throttle_delay_ms=${metrics.throttleDelayMs || 0} rate_limit_retries=${metrics.rateLimitRetries || 0} ` +
446
+ `checkpoint_completed=${normalized.length}/${texts.length} error=${error.message}`
447
+ );
448
+ }
449
+ throw error;
450
+ }
451
+ } else {
452
+ logRag(`checkpoint already complete provider=${name} completed=${resumeFrom}/${texts.length}`);
453
+ }
454
+
455
+ const payload = {
456
+ cacheKey,
457
+ meta: cacheMeta,
458
+ items: indexedItems,
459
+ vectors: normalized
460
+ };
461
+ vectorCache.saveVectorIndexCache(cacheFile, payload);
462
+ vectorCache.clearVectorIndexCheckpoint(checkpointFile);
463
+ logRag(`cache saved provider=${name} file=${cacheFile} items=${payload.items.length} vectors=${payload.vectors.length}`);
464
+ return {
465
+ items: payload.items,
466
+ vectors: payload.vectors
467
+ };
468
+ })();
469
+ return indexPromise;
470
+ };
471
+
472
+ return {
473
+ name,
474
+ search: async (query, filters, limit) => {
475
+ const prepared = utils.truncateText(utils.normalizeText(query), ragConfig.maxTextChars);
476
+ if (!prepared) return [];
477
+ const index = await loadIndex();
478
+ const queryVector = utils.normalizeVector(await embedder.embed(prepared));
479
+ const bestByUri = new Map();
480
+
481
+ for (let i = 0; i < index.vectors.length; i++) {
482
+ const score = utils.dotProduct(queryVector, index.vectors[i]);
483
+ if (ragConfig.minScore && score < ragConfig.minScore) continue;
484
+ const item = index.items[i];
485
+ const entry = resourceIndexByUri.get(item.uri);
486
+ if (!entry || !utils.entryMatchesScope(entry, filters)) continue;
487
+ const existing = bestByUri.get(item.uri);
488
+ if (!existing || score > existing.score) {
489
+ bestByUri.set(item.uri, { entry, score });
490
+ }
491
+ }
492
+
493
+ const results = Array.from(bestByUri.values())
494
+ .sort((a, b) => b.score - a.score)
495
+ .map((item) => utils.attachScore(item.entry, item.score));
496
+
497
+ if (limit) return results.slice(0, limit);
498
+ return results;
499
+ },
500
+ warm: async () => {
501
+ await loadIndex();
502
+ }
503
+ };
504
+ }
505
+
506
+ function createFuseProvider() {
507
+ return {
508
+ name: "fuse",
509
+ search: async (query, filters, limit) => {
510
+ const results = [];
511
+ for (const result of fuseSearch.search(query)) {
512
+ const entry = result.item;
513
+ if (!utils.entryMatchesScope(entry, filters)) continue;
514
+ const score = Number.isFinite(result.score) ? Math.max(0, 1 - result.score) : undefined;
515
+ results.push(utils.attachScore(entry, score));
516
+ }
517
+ if (limit) return results.slice(0, limit);
518
+ return results;
519
+ },
520
+ warm: async () => {}
521
+ };
522
+ }
523
+
524
+ function refreshProviders() {
525
+ fuseSearch = utils.createFuseSearch(resourceIndex);
526
+ providerCache.clear();
527
+ }
528
+
529
+ async function loadSearchProvider(name) {
530
+ if (providerCache.has(name)) return providerCache.get(name);
531
+ let providerPromise;
532
+ if (name === "fuse") {
533
+ providerPromise = Promise.resolve(createFuseProvider());
534
+ } else if (name === "lexical") {
535
+ providerPromise = Promise.resolve(createLexicalProvider({
536
+ entries: resourceIndex,
537
+ entryMatchesScope: utils.entryMatchesScope,
538
+ attachScore: utils.attachScore
539
+ }));
540
+ } else if (name === "local") {
541
+ providerPromise = (async () => {
542
+ const embedder = await getLocalEmbedder();
543
+ return createVectorProvider({
544
+ name: "local",
545
+ model: ragConfig.localModel,
546
+ embedder,
547
+ batchSize: 1
548
+ });
549
+ })();
550
+ } else if (name === "gemini") {
551
+ providerPromise = (async () => {
552
+ const embedder = await getGeminiEmbedder();
553
+ return createVectorProvider({
554
+ name: "gemini",
555
+ model: ragConfig.geminiModel,
556
+ embedder,
557
+ batchSize: Math.max(1, ragConfig.geminiBatchSize)
558
+ });
559
+ })();
560
+ } else {
561
+ providerPromise = Promise.reject(new Error(`Unknown search provider: ${name}`));
562
+ }
563
+ if (!ragLogState.providerReady.has(name)) {
564
+ ragLogState.providerReady.add(name);
565
+ logRag("provider_ready", {
566
+ profile: ragConfig.profile,
567
+ provider: name,
568
+ fallback: ragConfig.fallback
569
+ });
570
+ }
571
+ providerCache.set(name, providerPromise);
572
+ return providerPromise;
573
+ }
574
+
575
+ return {
576
+ resolveProviderChain: () => resolveProviderChain(ragConfig),
577
+ loadSearchProvider,
578
+ refreshProviders
579
+ };
580
+ }
581
+
582
+ export {
583
+ createProviderOrchestrator,
584
+ resolveProviderChain
585
+ };