local.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. /**
  2. * local.ts - Local llama.cpp adapter implementing EmbeddingProvider.
  3. *
  4. * Wraps an existing `LlamaCpp` instance so the legacy GGUF path looks like
  5. * any other EmbeddingProvider to upstream callers. Used as the default and
  6. * as the fallback target when `OpenAIEmbeddingsProvider` trips its breaker.
  7. */
  8. import { getDefaultLlamaCpp, } from "../llm.js";
  9. export class LocalLlamaCppProvider {
  10. kind = "local";
  11. llm;
  12. modelId;
  13. dimensions = undefined;
  14. lastError = undefined;
  15. constructor(config = {}) {
  16. this.llm = config.llm ?? getDefaultLlamaCpp();
  17. this.modelId = config.modelId ?? "embeddinggemma";
  18. }
  19. getModelId() {
  20. return this.modelId;
  21. }
  22. getDimensions() {
  23. return this.dimensions;
  24. }
  25. /**
  26. * Most recent thrown error from `llm.embed` / `llm.embedBatch`. Returns
  27. * `undefined` after a successful call or before the first call. See
  28. * `EmbeddingProvider.getLastError`.
  29. */
  30. getLastError() {
  31. return this.lastError;
  32. }
  33. async healthcheck(_signal) {
  34. // For the local provider, "healthy" means the embed model loads.
  35. // We probe with a single embed call.
  36. try {
  37. const result = await this.llm.embed("healthcheck", { model: this.modelId });
  38. if (!result) {
  39. return {
  40. ok: false,
  41. model: this.modelId,
  42. detail: "embed probe returned null",
  43. };
  44. }
  45. this.dimensions = result.embedding.length;
  46. return {
  47. ok: true,
  48. model: this.modelId,
  49. dimensions: this.dimensions,
  50. detail: `local llama.cpp ready, ${this.dimensions}-d`,
  51. };
  52. }
  53. catch (err) {
  54. return {
  55. ok: false,
  56. model: this.modelId,
  57. detail: err instanceof Error ? err.message : String(err),
  58. };
  59. }
  60. }
  61. async embed(text, options = {}) {
  62. if (options.signal?.aborted) {
  63. this.lastError = `aborted by caller${options.signal.reason ? `: ${String(options.signal.reason)}` : ""}`;
  64. return null;
  65. }
  66. let result;
  67. try {
  68. result = await this.llm.embed(text, { model: options.model ?? this.modelId });
  69. }
  70. catch (err) {
  71. this.lastError = `provider=local error="${err instanceof Error ? err.message : String(err)}"`;
  72. return null;
  73. }
  74. if (!result) {
  75. this.lastError = `provider=local error="llm.embed returned null/undefined"`;
  76. return null;
  77. }
  78. if (this.dimensions === undefined) {
  79. this.dimensions = result.embedding.length;
  80. }
  81. this.lastError = undefined;
  82. return {
  83. embedding: result.embedding,
  84. model: this.modelId,
  85. };
  86. }
  87. async embedBatch(texts, options = {}) {
  88. if (texts.length === 0)
  89. return [];
  90. if (options.signal?.aborted) {
  91. this.lastError = `aborted by caller${options.signal.reason ? `: ${String(options.signal.reason)}` : ""}`;
  92. return texts.map(() => null);
  93. }
  94. let raw;
  95. try {
  96. raw = await this.llm.embedBatch(texts, {
  97. model: options.model ?? this.modelId,
  98. });
  99. }
  100. catch (err) {
  101. this.lastError = `provider=local error="${err instanceof Error ? err.message : String(err)}"`;
  102. return texts.map(() => null);
  103. }
  104. const out = raw.map((r) => {
  105. if (!r)
  106. return null;
  107. if (this.dimensions === undefined && r.embedding.length > 0) {
  108. this.dimensions = r.embedding.length;
  109. }
  110. return {
  111. embedding: r.embedding,
  112. model: this.modelId,
  113. };
  114. });
  115. if (out.every((r) => r !== null)) {
  116. this.lastError = undefined;
  117. }
  118. else if (out.some((r) => r === null)) {
  119. this.lastError = `provider=local error="llm.embedBatch returned null entries (${out.filter((r) => r === null).length}/${out.length})"`;
  120. }
  121. return out;
  122. }
  123. async dispose() {
  124. // We do NOT dispose the underlying LlamaCpp here because the singleton
  125. // is shared with rerank/generate/expansion paths. Disposal is handled
  126. // by the existing `disposeDefaultLlamaCpp()` global hook.
  127. }
  128. }