localm-web 0.2.0 → 0.4.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/dist/index.js CHANGED
@@ -30,6 +30,43 @@ class QuotaExceededError extends LocalmWebError {
30
30
  }
31
31
  class BackendNotAvailableError extends LocalmWebError {
32
32
  }
33
+ class StructuredOutputError extends LocalmWebError {
34
+ }
35
+ function assertJsonSchema(schema) {
36
+ if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
37
+ throw new StructuredOutputError("jsonSchema must be a plain object describing a JSON Schema.");
38
+ }
39
+ const keys = Object.keys(schema);
40
+ const recognized = [
41
+ "type",
42
+ "$ref",
43
+ "oneOf",
44
+ "anyOf",
45
+ "allOf",
46
+ "enum",
47
+ "const",
48
+ "properties"
49
+ ];
50
+ if (!keys.some((key) => recognized.includes(key))) {
51
+ throw new StructuredOutputError(
52
+ "jsonSchema does not look like a JSON Schema (missing type/$ref/oneOf/anyOf/allOf/enum/const/properties)."
53
+ );
54
+ }
55
+ }
56
+ function serializeJsonSchema(schema) {
57
+ assertJsonSchema(schema);
58
+ return JSON.stringify(schema);
59
+ }
60
+ function parseStructuredOutput(text) {
61
+ try {
62
+ return JSON.parse(text);
63
+ } catch (err) {
64
+ throw new StructuredOutputError(
65
+ "Engine output is not valid JSON. The model may have ignored the constrained decoding directive.",
66
+ err
67
+ );
68
+ }
69
+ }
33
70
  let webllmModulePromise = null;
34
71
  async function loadWebLLM() {
35
72
  if (!webllmModulePromise) {
@@ -47,6 +84,15 @@ function buildSamplingParams(options) {
47
84
  if (options.topP !== void 0) params.top_p = options.topP;
48
85
  return params;
49
86
  }
87
+ function buildResponseFormat(options) {
88
+ if (options.jsonSchema !== void 0) {
89
+ return { type: "json_object", schema: serializeJsonSchema(options.jsonSchema) };
90
+ }
91
+ if (options.json) {
92
+ return { type: "json_object" };
93
+ }
94
+ return void 0;
95
+ }
50
96
  function toChatMessages(messages) {
51
97
  return messages.map((m) => {
52
98
  switch (m.role) {
@@ -101,10 +147,12 @@ class WebLLMEngine {
101
147
  if (options.signal?.aborted) {
102
148
  throw new GenerationAbortedError("Generation aborted before start.");
103
149
  }
150
+ const responseFormat = buildResponseFormat(options);
104
151
  const completion = await engine.chat.completions.create({
105
152
  ...buildSamplingParams(options),
106
153
  messages: toChatMessages(messages),
107
- stream: false
154
+ stream: false,
155
+ ...responseFormat ? { response_format: responseFormat } : {}
108
156
  });
109
157
  return completion.choices[0]?.message?.content ?? "";
110
158
  }
@@ -113,10 +161,12 @@ class WebLLMEngine {
113
161
  if (options.signal?.aborted) {
114
162
  throw new GenerationAbortedError("Generation aborted before start.");
115
163
  }
164
+ const responseFormat = buildResponseFormat(options);
116
165
  const completion = await engine.chat.completions.create({
117
166
  ...buildSamplingParams(options),
118
167
  messages: toChatMessages(messages),
119
- stream: true
168
+ stream: true,
169
+ ...responseFormat ? { response_format: responseFormat } : {}
120
170
  });
121
171
  let index = 0;
122
172
  let finished = false;
@@ -150,10 +200,12 @@ class WebLLMEngine {
150
200
  if (options.signal?.aborted) {
151
201
  throw new GenerationAbortedError("Generation aborted before start.");
152
202
  }
203
+ const responseFormat = buildResponseFormat(options);
153
204
  const completion = await engine.completions.create({
154
205
  ...buildSamplingParams(options),
155
206
  prompt,
156
- stream: false
207
+ stream: false,
208
+ ...responseFormat ? { response_format: responseFormat } : {}
157
209
  });
158
210
  return completion.choices[0]?.text ?? "";
159
211
  }
@@ -162,10 +214,12 @@ class WebLLMEngine {
162
214
  if (options.signal?.aborted) {
163
215
  throw new GenerationAbortedError("Generation aborted before start.");
164
216
  }
217
+ const responseFormat = buildResponseFormat(options);
165
218
  const completion = await engine.completions.create({
166
219
  ...buildSamplingParams(options),
167
220
  prompt,
168
- stream: true
221
+ stream: true,
222
+ ...responseFormat ? { response_format: responseFormat } : {}
169
223
  });
170
224
  let index = 0;
171
225
  let finished = false;
@@ -524,10 +578,66 @@ function resolveModelPreset(modelId) {
524
578
  function listSupportedModels() {
525
579
  return Object.keys(MODEL_PRESETS);
526
580
  }
581
+ const EMBEDDING_PRESETS = Object.freeze({
582
+ "bge-small-en-v1.5": {
583
+ id: "bge-small-en-v1.5",
584
+ family: "BGE",
585
+ dimension: 384,
586
+ maxTokens: 512,
587
+ transformersId: "Xenova/bge-small-en-v1.5",
588
+ quantization: "fp32",
589
+ description: "BAAI BGE small English v1.5, 384-dim sentence embeddings."
590
+ },
591
+ "bge-base-en-v1.5": {
592
+ id: "bge-base-en-v1.5",
593
+ family: "BGE",
594
+ dimension: 768,
595
+ maxTokens: 512,
596
+ transformersId: "Xenova/bge-base-en-v1.5",
597
+ quantization: "fp32",
598
+ description: "BAAI BGE base English v1.5, 768-dim sentence embeddings."
599
+ }
600
+ });
601
+ function resolveEmbeddingPreset(modelId) {
602
+ const preset = EMBEDDING_PRESETS[modelId];
603
+ if (!preset) {
604
+ const available = Object.keys(EMBEDDING_PRESETS).join(", ");
605
+ throw new UnknownModelError(
606
+ `Unknown embedding model "${modelId}". Available models: ${available}.`
607
+ );
608
+ }
609
+ return preset;
610
+ }
611
+ function listSupportedEmbeddingModels() {
612
+ return Object.keys(EMBEDDING_PRESETS);
613
+ }
614
+ const RERANKER_PRESETS = Object.freeze({
615
+ "bge-reranker-base": {
616
+ id: "bge-reranker-base",
617
+ family: "BGE Reranker",
618
+ maxTokens: 512,
619
+ transformersId: "Xenova/bge-reranker-base",
620
+ quantization: "fp32",
621
+ description: "BAAI BGE reranker base — multilingual cross-encoder."
622
+ }
623
+ });
624
+ function resolveRerankerPreset(modelId) {
625
+ const preset = RERANKER_PRESETS[modelId];
626
+ if (!preset) {
627
+ const available = Object.keys(RERANKER_PRESETS).join(", ");
628
+ throw new UnknownModelError(
629
+ `Unknown reranker model "${modelId}". Available models: ${available}.`
630
+ );
631
+ }
632
+ return preset;
633
+ }
634
+ function listSupportedRerankerModels() {
635
+ return Object.keys(RERANKER_PRESETS);
636
+ }
527
637
  function createInferenceWorker() {
528
638
  return new Worker(new URL(
529
639
  /* @vite-ignore */
530
- "/assets/inference.worker-CwvQtobb.js",
640
+ "/assets/inference.worker-DZbXKJZY.js",
531
641
  import.meta.url
532
642
  ), {
533
643
  type: "module"
@@ -555,7 +665,8 @@ class LMTask {
555
665
  return { engine, preset };
556
666
  }
557
667
  static defaultEngine(options) {
558
- if (options.inWorker) {
668
+ const useWorker = options.inWorker ?? true;
669
+ if (useWorker) {
559
670
  return new WorkerEngine(createInferenceWorker());
560
671
  }
561
672
  return new WebLLMEngine();
@@ -576,6 +687,20 @@ class ChatReply {
576
687
  this.tokensGenerated = tokensGenerated;
577
688
  this.finishReason = finishReason;
578
689
  }
690
+ /**
691
+ * Parse {@link ChatReply.text} as JSON.
692
+ *
693
+ * Intended for replies generated with `json: true` or `jsonSchema`.
694
+ * The result is cast to `T` without runtime validation; pair with Zod /
695
+ * Ajv on the call site if you need to verify the schema.
696
+ *
697
+ * @typeParam T - Expected parsed shape.
698
+ * @returns The parsed JSON value.
699
+ * @throws StructuredOutputError if the text is not valid JSON.
700
+ */
701
+ json() {
702
+ return parseStructuredOutput(this.text);
703
+ }
579
704
  }
580
705
  class CompletionResult {
581
706
  constructor(text, prompt, tokensGenerated, finishReason) {
@@ -584,6 +709,19 @@ class CompletionResult {
584
709
  this.tokensGenerated = tokensGenerated;
585
710
  this.finishReason = finishReason;
586
711
  }
712
+ /**
713
+ * Parse {@link CompletionResult.text} as JSON.
714
+ *
715
+ * Intended for completions generated with `json: true` or `jsonSchema`.
716
+ * The result is cast to `T` without runtime validation.
717
+ *
718
+ * @typeParam T - Expected parsed shape.
719
+ * @returns The parsed JSON value.
720
+ * @throws StructuredOutputError if the text is not valid JSON.
721
+ */
722
+ json() {
723
+ return parseStructuredOutput(this.text);
724
+ }
587
725
  }
588
726
  class Chat extends LMTask {
589
727
  history = [];
@@ -702,6 +840,230 @@ class Completion extends LMTask {
702
840
  }
703
841
  }
704
842
  }
843
+ let transformersModulePromise$1 = null;
844
+ async function loadTransformers$1() {
845
+ if (!transformersModulePromise$1) {
846
+ transformersModulePromise$1 = import("@huggingface/transformers");
847
+ }
848
+ return transformersModulePromise$1;
849
+ }
850
+ async function buildDefaultPipeline$1(preset, onProgress) {
851
+ const transformers = await loadTransformers$1();
852
+ try {
853
+ const pipe = await transformers.pipeline("feature-extraction", preset.transformersId, {
854
+ progress_callback: (report) => {
855
+ if (!onProgress) return;
856
+ const r = report;
857
+ onProgress({
858
+ progress: typeof r.progress === "number" ? r.progress / 100 : 0,
859
+ text: r.status ?? "",
860
+ loaded: 0,
861
+ total: 0,
862
+ phase: "downloading"
863
+ });
864
+ }
865
+ });
866
+ return {
867
+ async embed(texts, options) {
868
+ const output = await pipe(texts, {
869
+ pooling: options.pooling,
870
+ normalize: options.normalize
871
+ });
872
+ return output.tolist();
873
+ },
874
+ async unload() {
875
+ if (typeof pipe.dispose === "function") {
876
+ await pipe.dispose();
877
+ }
878
+ }
879
+ };
880
+ } catch (err) {
881
+ throw new ModelLoadError(`Failed to load embedding model "${preset.id}".`, err);
882
+ }
883
+ }
884
+ class Embeddings {
885
+ constructor(pipeline, preset) {
886
+ this.pipeline = pipeline;
887
+ this.preset = preset;
888
+ }
889
+ /**
890
+ * Create and load an `Embeddings` task for the given model.
891
+ *
892
+ * @param modelId - Friendly id from the embedding registry.
893
+ * @param options - Optional creation options.
894
+ * @throws UnknownModelError if `modelId` is not in the registry.
895
+ * @throws ModelLoadError if the underlying pipeline fails to load.
896
+ */
897
+ static async create(modelId, options = {}) {
898
+ const preset = resolveEmbeddingPreset(modelId);
899
+ const pipeline = options.pipeline ?? await buildDefaultPipeline$1(preset, options.onProgress);
900
+ return new Embeddings(pipeline, preset);
901
+ }
902
+ /**
903
+ * Encode an array of strings into dense vectors.
904
+ *
905
+ * Returns one vector per input, in the same order. Empty input array
906
+ * returns an empty array (no error).
907
+ *
908
+ * @param texts - Input strings.
909
+ * @param options - Pooling + normalization. Defaults: `pooling: "mean"`, `normalize: true`.
910
+ */
911
+ async embed(texts, options = {}) {
912
+ if (texts.length === 0) return [];
913
+ if (!this.pipeline) {
914
+ throw new ModelNotLoadedError("Embeddings pipeline not initialized.");
915
+ }
916
+ const merged = {
917
+ normalize: options.normalize ?? true,
918
+ pooling: options.pooling ?? "mean"
919
+ };
920
+ return this.pipeline.embed(texts, merged);
921
+ }
922
+ /**
923
+ * Convenience: encode a single string and return its vector.
924
+ *
925
+ * @param text - Input string.
926
+ * @param options - Forwarded to {@link Embeddings.embed}.
927
+ */
928
+ async embedSingle(text, options = {}) {
929
+ const [vec] = await this.embed([text], options);
930
+ if (!vec) {
931
+ throw new ModelLoadError("Embedding pipeline returned no result.");
932
+ }
933
+ return vec;
934
+ }
935
+ /** Embedding dimension exposed by the loaded model. */
936
+ get dimension() {
937
+ return this.preset.dimension;
938
+ }
939
+ /** Release pipeline resources. Safe to call multiple times. */
940
+ async unload() {
941
+ await this.pipeline.unload?.();
942
+ }
943
+ }
944
+ let transformersModulePromise = null;
945
+ async function loadTransformers() {
946
+ if (!transformersModulePromise) {
947
+ transformersModulePromise = import("@huggingface/transformers");
948
+ }
949
+ return transformersModulePromise;
950
+ }
951
+ function sigmoidValue(x) {
952
+ return 1 / (1 + Math.exp(-x));
953
+ }
954
+ async function buildDefaultPipeline(preset, onProgress) {
955
+ const transformers = await loadTransformers();
956
+ try {
957
+ const tokenizer = await transformers.AutoTokenizer.from_pretrained(preset.transformersId, {
958
+ progress_callback: (report) => {
959
+ if (!onProgress) return;
960
+ const r = report;
961
+ onProgress({
962
+ progress: typeof r.progress === "number" ? r.progress / 100 : 0,
963
+ text: r.status ?? "",
964
+ loaded: 0,
965
+ total: 0,
966
+ phase: "downloading"
967
+ });
968
+ }
969
+ });
970
+ const model = await transformers.AutoModelForSequenceClassification.from_pretrained(
971
+ preset.transformersId,
972
+ {
973
+ progress_callback: (report) => {
974
+ if (!onProgress) return;
975
+ const r = report;
976
+ onProgress({
977
+ progress: typeof r.progress === "number" ? r.progress / 100 : 0,
978
+ text: r.status ?? "",
979
+ loaded: 0,
980
+ total: 0,
981
+ phase: "downloading"
982
+ });
983
+ }
984
+ }
985
+ );
986
+ return {
987
+ async score(query, docs) {
988
+ if (docs.length === 0) return [];
989
+ const queries = docs.map(() => query);
990
+ const tokenize = tokenizer;
991
+ const inputs = tokenize(queries, {
992
+ text_pair: docs,
993
+ padding: true,
994
+ truncation: true,
995
+ max_length: preset.maxTokens
996
+ });
997
+ const callModel = model;
998
+ const outputs = await callModel(inputs);
999
+ const logits = outputs.logits.tolist();
1000
+ return logits.map((row) => row[0] ?? 0);
1001
+ },
1002
+ async unload() {
1003
+ const m = model;
1004
+ if (typeof m.dispose === "function") await m.dispose();
1005
+ }
1006
+ };
1007
+ } catch (err) {
1008
+ throw new ModelLoadError(`Failed to load reranker model "${preset.id}".`, err);
1009
+ }
1010
+ }
1011
+ class Reranker {
1012
+ constructor(pipeline, preset) {
1013
+ this.pipeline = pipeline;
1014
+ this.preset = preset;
1015
+ }
1016
+ /**
1017
+ * Create and load a `Reranker` task for the given model.
1018
+ *
1019
+ * @param modelId - Friendly id from the reranker registry.
1020
+ * @param options - Optional creation options.
1021
+ * @throws UnknownModelError if `modelId` is not in the registry.
1022
+ * @throws ModelLoadError if the underlying pipeline fails to load.
1023
+ */
1024
+ static async create(modelId, options = {}) {
1025
+ const preset = resolveRerankerPreset(modelId);
1026
+ const pipeline = options.pipeline ?? await buildDefaultPipeline(preset, options.onProgress);
1027
+ return new Reranker(pipeline, preset);
1028
+ }
1029
+ /**
1030
+ * Score each document against the query. Returns one score per doc, in
1031
+ * the same order. Empty `docs` returns `[]` (no error).
1032
+ *
1033
+ * @param query - Query string.
1034
+ * @param docs - Documents to score.
1035
+ * @param options - `sigmoid: true` maps logits into `[0, 1]`.
1036
+ */
1037
+ async score(query, docs, options = {}) {
1038
+ if (docs.length === 0) return [];
1039
+ if (!this.pipeline) {
1040
+ throw new ModelNotLoadedError("Reranker pipeline not initialized.");
1041
+ }
1042
+ const raw = await this.pipeline.score(query, docs);
1043
+ return options.sigmoid ? raw.map(sigmoidValue) : raw;
1044
+ }
1045
+ /**
1046
+ * Score and sort documents by score in descending order. Returns a list of
1047
+ * {@link RankedDocument}s carrying the original index.
1048
+ *
1049
+ * @param query - Query string.
1050
+ * @param docs - Documents to rank.
1051
+ * @param options - Forwarded to {@link Reranker.score}.
1052
+ */
1053
+ async rank(query, docs, options = {}) {
1054
+ const scores = await this.score(query, docs, options);
1055
+ const ranked = scores.map((score, index) => {
1056
+ const text = docs[index] ?? "";
1057
+ return { text, score, index };
1058
+ });
1059
+ ranked.sort((a, b) => b.score - a.score);
1060
+ return ranked;
1061
+ }
1062
+ /** Release pipeline resources. Safe to call multiple times. */
1063
+ async unload() {
1064
+ await this.pipeline.unload?.();
1065
+ }
1066
+ }
705
1067
  let webllmCachePromise = null;
706
1068
  async function loadWebLLMCacheHelpers() {
707
1069
  if (!webllmCachePromise) {
@@ -823,13 +1185,15 @@ async function* tap(stream, onChunk) {
823
1185
  yield chunk;
824
1186
  }
825
1187
  }
826
- const VERSION = "0.2.0";
1188
+ const VERSION = "0.4.0";
827
1189
  export {
828
1190
  BackendNotAvailableError,
829
1191
  Chat,
830
1192
  ChatReply,
831
1193
  Completion,
832
1194
  CompletionResult,
1195
+ EMBEDDING_PRESETS,
1196
+ Embeddings,
833
1197
  GenerationAbortedError,
834
1198
  LMTask,
835
1199
  LocalmWebError,
@@ -838,14 +1202,24 @@ export {
838
1202
  ModelLoadError,
839
1203
  ModelNotLoadedError,
840
1204
  QuotaExceededError,
1205
+ RERANKER_PRESETS,
1206
+ Reranker,
1207
+ StructuredOutputError,
841
1208
  UnknownModelError,
842
1209
  VERSION,
843
1210
  WebGPUUnavailableError,
844
1211
  WorkerEngine,
1212
+ assertJsonSchema,
845
1213
  collectStream,
846
1214
  createInferenceWorker,
1215
+ listSupportedEmbeddingModels,
847
1216
  listSupportedModels,
1217
+ listSupportedRerankerModels,
1218
+ parseStructuredOutput,
1219
+ resolveEmbeddingPreset,
848
1220
  resolveModelPreset,
1221
+ resolveRerankerPreset,
1222
+ serializeJsonSchema,
849
1223
  tap
850
1224
  };
851
1225
  //# sourceMappingURL=index.js.map