@backbencher/brain 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.
- package/CHANGELOG.md +298 -0
- package/EMBEDDINGS.md +355 -0
- package/LICENSE +21 -0
- package/README.md +415 -2
- package/bin/bb +13 -0
- package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
- package/dist/brain-index-health-KY7K552E.js +17 -0
- package/dist/brain-relationship-enrichment-QH7OOHUF.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -0
- package/dist/chunk-2BSKD7O3.js +40 -0
- package/dist/chunk-2DE27DVC.js +1314 -0
- package/dist/chunk-2RRBMNBQ.js +322 -0
- package/dist/chunk-2RTN2DFU.js +711 -0
- package/dist/chunk-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3LFTSMIV.js +174 -0
- package/dist/chunk-47HKX4AJ.js +930 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5SD45FLC.js +818 -0
- package/dist/chunk-66A5ZFYA.js +1758 -0
- package/dist/chunk-6AIRWAO2.js +247 -0
- package/dist/chunk-6WNGD3NN.js +196 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-BHB7BYYI.js +626 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-CW4XXSNN.js +1479 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-FVVQTSIM.js +3427 -0
- package/dist/chunk-FWSL4VG6.js +1290 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HOWAQXJO.js +236 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-IFZNR5UJ.js +2863 -0
- package/dist/chunk-J4PMBXQQ.js +336 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-KLK3PBTO.js +1119 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-PJTNGD3Z.js +6924 -0
- package/dist/chunk-QLK6ED4Q.js +235 -0
- package/dist/chunk-R2GW4F6R.js +19 -0
- package/dist/chunk-RCMTFSEF.js +87 -0
- package/dist/chunk-RH7KC5HU.js +12050 -0
- package/dist/chunk-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UFE77L67.js +782 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-V44PGPA6.js +2104 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-VURYFIHW.js +201 -0
- package/dist/chunk-XJKJYIPP.js +7436 -0
- package/dist/chunk-YI6FF2ZR.js +340 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-642F7E5V.js +593 -0
- package/dist/database-TCTZDLE5.js +9 -0
- package/dist/doctor-GXCKSJOX.js +395 -0
- package/dist/edit-engine-KM7MPFEC.js +51 -0
- package/dist/engine-42TZFNIZ.js +11 -0
- package/dist/engine-LO6L4JLQ.js +18 -0
- package/dist/fingerprint-XDXFRWO6.js +33 -0
- package/dist/framework-intelligence-DWFCKV44.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-PL5YA7BA.js +21 -0
- package/dist/graph_request-MM4REP6H.js +350 -0
- package/dist/impact_request-WDE47IZ5.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-6BD7UBHR.js +34 -0
- package/dist/index.js +159 -0
- package/dist/instance-FO6QZRLH.js +19 -0
- package/dist/language-detection-DFCAU5DZ.js +11 -0
- package/dist/lsp/server.js +4301 -0
- package/dist/mcp/server.js +132 -0
- package/dist/onboard-DIQZV2NW.js +161 -0
- package/dist/override-companion-pass-XR7MHTP7.js +117 -0
- package/dist/parallel-indexer-4PW3AFXP.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-ZS5TS2Z6.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-DGZBIZS7.js +355 -0
- package/dist/real-bash-analyzer-SXSV67E2.js +2201 -0
- package/dist/real-c-analyzer-SY7FTZ6W.js +5942 -0
- package/dist/real-clean-analyzer-IGDHHLZK.js +3470 -0
- package/dist/real-clojure-analyzer-4OH7IWLB.js +2818 -0
- package/dist/real-cpp-analyzer-7LTDRJRJ.js +4101 -0
- package/dist/real-csharp-analyzer-KICU3TLH.js +3298 -0
- package/dist/real-css-analyzer-HPU277Q6.js +15 -0
- package/dist/real-dart-analyzer-7LZ5LCMW.js +3944 -0
- package/dist/real-elixir-analyzer-BE3MEVZO.js +2192 -0
- package/dist/real-erlang-analyzer-BZMAKYBK.js +1731 -0
- package/dist/real-fsharp-analyzer-5QLTOV3E.js +2390 -0
- package/dist/real-go-analyzer-POD7MZQR.js +4126 -0
- package/dist/real-graphql-analyzer-MAQRZ6QW.js +1442 -0
- package/dist/real-groovy-analyzer-MEWSUPMJ.js +2285 -0
- package/dist/real-haskell-analyzer-I5L2GR3Y.js +2533 -0
- package/dist/real-hcl-analyzer-DBOPXSWJ.js +725 -0
- package/dist/real-html-analyzer-O63XZLZD.js +1759 -0
- package/dist/real-java-analyzer-CHCW3EKA.js +4677 -0
- package/dist/real-javascript-analyzer-QAAJQYZE.js +14 -0
- package/dist/real-json-analyzer-UGBR7YQN.js +1640 -0
- package/dist/real-julia-analyzer-35Q4GHOZ.js +2085 -0
- package/dist/real-kotlin-analyzer-GRAYCWW2.js +3956 -0
- package/dist/real-lua-analyzer-5BAPDZDF.js +1957 -0
- package/dist/real-makefile-analyzer-YJKAHNAS.js +920 -0
- package/dist/real-markdown-analyzer-UKUQZO4F.js +1215 -0
- package/dist/real-matlab-analyzer-FVIKQZKG.js +480 -0
- package/dist/real-nim-analyzer-M7FAIABG.js +3603 -0
- package/dist/real-objective-c-analyzer-XMYQIGQI.js +1518 -0
- package/dist/real-perl-analyzer-ZLY6M2DZ.js +1865 -0
- package/dist/real-php-analyzer-KI2ZAM7X.js +9368 -0
- package/dist/real-powershell-analyzer-B3T5ZP6U.js +2915 -0
- package/dist/real-protobuf-analyzer-23OZ75XV.js +867 -0
- package/dist/real-python-analyzer-I4TIUB6D.js +4880 -0
- package/dist/real-r-analyzer-FJZJXXS6.js +2596 -0
- package/dist/real-ruby-analyzer-M3UFIVTQ.js +2429 -0
- package/dist/real-rust-analyzer-LVSONPAN.js +3359 -0
- package/dist/real-scala-analyzer-CWPI33Q2.js +3293 -0
- package/dist/real-scss-analyzer-TGRJQ7TH.js +1515 -0
- package/dist/real-solidity-analyzer-4P2VBVWA.js +2608 -0
- package/dist/real-sql-analyzer-J7DYZOZR.js +3840 -0
- package/dist/real-svelte-analyzer-NHEN2SX5.js +2532 -0
- package/dist/real-swift-analyzer-2NSYULWI.js +2311 -0
- package/dist/real-toml-analyzer-L3C66L7Q.js +929 -0
- package/dist/real-typescript-analyzer-FTC6ETRQ.js +14 -0
- package/dist/real-vb-analyzer-ORY7LAQS.js +3145 -0
- package/dist/real-vba-analyzer-JH2TTUFI.js +466 -0
- package/dist/real-vue-analyzer-OO4FUGBV.js +2680 -0
- package/dist/real-xml-analyzer-6CQVR477.js +2405 -0
- package/dist/real-yaml-analyzer-J6MQG3YX.js +3528 -0
- package/dist/real-zig-analyzer-EFKCPJNU.js +2430 -0
- package/dist/regex-search-OPTLKUN5.js +239 -0
- package/dist/rules-I453JTRC.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-Y5GB5KXA.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-KB4MU4SI.js +16 -0
- package/dist/taint-YOTN5X55.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-G4NAGJIH.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-RF675A3C.js +32 -0
- package/dist/type-inference-4Y7WTMFE.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-LXGNFO37.js +9 -0
- package/dist/wasm-NMWO3WXY.js +16 -0
- package/docs/EMBEDDINGS.md +132 -0
- package/docs/README.md +178 -0
- package/docs/data/m10-baseline.json +602 -0
- package/docs/data/m10-benchmark-baseline.json +57 -0
- package/docs/data/m10-benchmark-last-run.json +127 -0
- package/docs/data/m10-final-audit.json +215 -0
- package/docs/data/m12-final-audit.json +138 -0
- package/docs/data/m12-surface-matrix.json +50 -0
- package/docs/data/public-tools.json +1902 -0
- package/docs/indexing/DEAD-ASSETS.md +88 -0
- package/docs/indexing/INDEXER-REVIEW.md +634 -0
- package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
- package/docs/indexing/README.md +427 -0
- package/docs/indexing/discovery-matrix.md +41 -0
- package/docs/indexing/embeddings.md +325 -0
- package/docs/indexing/environment-variables.md +77 -0
- package/docs/indexing/performance.md +323 -0
- package/docs/indexing/resume.md +345 -0
- package/docs/indexing/troubleshooting.md +391 -0
- package/docs/man/bb.1 +190 -0
- package/docs/tools/bb_analyze.md +147 -0
- package/docs/tools/bb_annotate.md +110 -0
- package/docs/tools/bb_annotations.md +106 -0
- package/docs/tools/bb_ask.md +101 -0
- package/docs/tools/bb_cross_language.md +103 -0
- package/docs/tools/bb_doc.md +169 -0
- package/docs/tools/bb_edit.md +185 -0
- package/docs/tools/bb_graph.md +169 -0
- package/docs/tools/bb_health.md +156 -0
- package/docs/tools/bb_impact.md +117 -0
- package/docs/tools/bb_index.md +108 -0
- package/docs/tools/bb_migrate.md +135 -0
- package/docs/tools/bb_patterns.md +140 -0
- package/docs/tools/bb_search.md +150 -0
- package/docs/tools/bb_security.md +100 -0
- package/docs/tools/bb_visualize.md +184 -0
- package/grammars/README.md +57 -0
- package/grammars/THIRD-PARTY-NOTICES.md +50 -0
- package/grammars/tree-sitter-c/LICENSE +21 -0
- package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
- package/grammars/tree-sitter-cpp/LICENSE +21 -0
- package/grammars/tree-sitter-csharp/LICENSE +21 -0
- package/grammars/tree-sitter-css/LICENSE +21 -0
- package/grammars/tree-sitter-css/README.md +21 -0
- package/grammars/tree-sitter-dart/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/README.md +18 -0
- package/grammars/tree-sitter-elixir/LICENSE +176 -0
- package/grammars/tree-sitter-elixir/README.md +11 -0
- package/grammars/tree-sitter-fsharp/LICENSE +21 -0
- package/grammars/tree-sitter-fsharp/README.md +45 -0
- package/grammars/tree-sitter-graphql/LICENSE +21 -0
- package/grammars/tree-sitter-graphql/README.md +106 -0
- package/grammars/tree-sitter-groovy/LICENSE +21 -0
- package/grammars/tree-sitter-groovy/README.md +4 -0
- package/grammars/tree-sitter-haskell/LICENSE +21 -0
- package/grammars/tree-sitter-haskell/README.md +394 -0
- package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
- package/grammars/tree-sitter-hcl/LICENSE +201 -0
- package/grammars/tree-sitter-hcl/README.md +39 -0
- package/grammars/tree-sitter-html/LICENSE +21 -0
- package/grammars/tree-sitter-html/README.md +21 -0
- package/grammars/tree-sitter-julia/LICENSE +21 -0
- package/grammars/tree-sitter-julia/README.md +24 -0
- package/grammars/tree-sitter-kotlin/LICENSE +9 -0
- package/grammars/tree-sitter-lua/LICENSE.md +21 -0
- package/grammars/tree-sitter-lua/README.md +29 -0
- package/grammars/tree-sitter-make/LICENSE +21 -0
- package/grammars/tree-sitter-matlab/LICENSE +19 -0
- package/grammars/tree-sitter-matlab/README.md +99 -0
- package/grammars/tree-sitter-nim/LICENSE +373 -0
- package/grammars/tree-sitter-nim/README.md +46 -0
- package/grammars/tree-sitter-objc/LICENSE +21 -0
- package/grammars/tree-sitter-objc/README.md +20 -0
- package/grammars/tree-sitter-perl/LICENSE +7 -0
- package/grammars/tree-sitter-perl/README.md +100 -0
- package/grammars/tree-sitter-powershell/LICENSE +21 -0
- package/grammars/tree-sitter-powershell/README.md +7 -0
- package/grammars/tree-sitter-proto/LICENSE +21 -0
- package/grammars/tree-sitter-proto/README.md +105 -0
- package/grammars/tree-sitter-r/LICENSE +9 -0
- package/grammars/tree-sitter-r/README.md +66 -0
- package/grammars/tree-sitter-ruby/LICENSE +22 -0
- package/grammars/tree-sitter-scss/LICENSE +21 -0
- package/grammars/tree-sitter-scss/README.md +17 -0
- package/grammars/tree-sitter-solidity/LICENSE +7 -0
- package/grammars/tree-sitter-sql/LICENSE +21 -0
- package/grammars/tree-sitter-typescript/LICENSE +21 -0
- package/grammars/tree-sitter-vba/LICENSE +21 -0
- package/grammars/tree-sitter-vba/README.md +738 -0
- package/grammars/tree-sitter-vbnet/LICENSE +21 -0
- package/grammars/tree-sitter-vbnet/README.md +322 -0
- package/grammars/tree-sitter-xml/LICENSE +19 -0
- package/grammars/tree-sitter-yaml/LICENSE +20 -0
- package/grammars/tree-sitter-yaml/README.md +21 -0
- package/package.json +184 -4
- package/wasm/dart.wasm +0 -0
- package/wasm/tree-sitter-bash.wasm +0 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-clean.wasm +0 -0
- package/wasm/tree-sitter-clojure.wasm +0 -0
- package/wasm/tree-sitter-core_schema.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-css.wasm +0 -0
- package/wasm/tree-sitter-dart.wasm +0 -0
- package/wasm/tree-sitter-elisp.wasm +0 -0
- package/wasm/tree-sitter-elixir.wasm +0 -0
- package/wasm/tree-sitter-elm.wasm +0 -0
- package/wasm/tree-sitter-embedded_template.wasm +0 -0
- package/wasm/tree-sitter-erlang.wasm +0 -0
- package/wasm/tree-sitter-fsharp.wasm +0 -0
- package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-graphql.wasm +0 -0
- package/wasm/tree-sitter-groovy.wasm +0 -0
- package/wasm/tree-sitter-haskell.wasm +0 -0
- package/wasm/tree-sitter-hcl.wasm +0 -0
- package/wasm/tree-sitter-html.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-json.wasm +0 -0
- package/wasm/tree-sitter-json_schema.wasm +0 -0
- package/wasm/tree-sitter-julia.wasm +0 -0
- package/wasm/tree-sitter-kotlin.wasm +0 -0
- package/wasm/tree-sitter-lua.wasm +0 -0
- package/wasm/tree-sitter-make.wasm +0 -0
- package/wasm/tree-sitter-markdown.wasm +0 -0
- package/wasm/tree-sitter-markdown_inline.wasm +0 -0
- package/wasm/tree-sitter-matlab.wasm +0 -0
- package/wasm/tree-sitter-nim.wasm +0 -0
- package/wasm/tree-sitter-objc.wasm +0 -0
- package/wasm/tree-sitter-ocaml.wasm +0 -0
- package/wasm/tree-sitter-perl.wasm +0 -0
- package/wasm/tree-sitter-php.wasm +0 -0
- package/wasm/tree-sitter-php_only.wasm +0 -0
- package/wasm/tree-sitter-powershell.wasm +0 -0
- package/wasm/tree-sitter-protobuf.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ql.wasm +0 -0
- package/wasm/tree-sitter-r.wasm +0 -0
- package/wasm/tree-sitter-rescript.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-scala.wasm +0 -0
- package/wasm/tree-sitter-scss.wasm +0 -0
- package/wasm/tree-sitter-solidity.wasm +0 -0
- package/wasm/tree-sitter-sql.wasm +0 -0
- package/wasm/tree-sitter-svelte.wasm +0 -0
- package/wasm/tree-sitter-swift.wasm +0 -0
- package/wasm/tree-sitter-systemrdl.wasm +0 -0
- package/wasm/tree-sitter-terraform.wasm +0 -0
- package/wasm/tree-sitter-tlaplus.wasm +0 -0
- package/wasm/tree-sitter-toml.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/tree-sitter-vba.wasm +0 -0
- package/wasm/tree-sitter-vbnet.wasm +0 -0
- package/wasm/tree-sitter-vue.wasm +0 -0
- package/wasm/tree-sitter-xml.wasm +0 -0
- package/wasm/tree-sitter-yaml.wasm +0 -0
- package/wasm/tree-sitter-zig.wasm +0 -0
- package/wasm/tree-sitter.wasm +0 -0
- package/wasm/tsx.wasm +0 -0
- package/wasm/typescript.wasm +0 -0
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
import {
|
|
2
|
+
embedLocal,
|
|
3
|
+
initLocalModel,
|
|
4
|
+
projectVocabulary
|
|
5
|
+
} from "./chunk-7AN7U5IZ.js";
|
|
6
|
+
import {
|
|
7
|
+
Config,
|
|
8
|
+
resolveEmbeddingConfig
|
|
9
|
+
} from "./chunk-UGR4QIGS.js";
|
|
10
|
+
import {
|
|
11
|
+
create
|
|
12
|
+
} from "./chunk-BQ3OVQ3P.js";
|
|
13
|
+
|
|
14
|
+
// src/brain/semantic/engine.ts
|
|
15
|
+
import { createHash } from "crypto";
|
|
16
|
+
import * as fs from "fs";
|
|
17
|
+
import * as path from "path";
|
|
18
|
+
|
|
19
|
+
// src/provider/provider.ts
|
|
20
|
+
var Provider;
|
|
21
|
+
((Provider2) => {
|
|
22
|
+
async function list() {
|
|
23
|
+
const config = await Config.get();
|
|
24
|
+
const providers = config.providers ?? {};
|
|
25
|
+
const result = {};
|
|
26
|
+
for (const [id, p] of Object.entries(providers)) {
|
|
27
|
+
result[id] = {
|
|
28
|
+
id,
|
|
29
|
+
baseURL: p?.baseURL ?? p?.baseUrl,
|
|
30
|
+
apiKey: p?.apiKey ?? p?.api_key,
|
|
31
|
+
models: p?.models ?? {}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (process.env.BB_EMBEDDING_BASE_URL) {
|
|
35
|
+
result["bb"] = {
|
|
36
|
+
id: "bb",
|
|
37
|
+
baseURL: process.env.BB_EMBEDDING_BASE_URL,
|
|
38
|
+
apiKey: process.env.BB_EMBEDDING_API_KEY,
|
|
39
|
+
models: { [process.env.BB_EMBEDDING_MODEL ?? "text-embedding-3-small"]: { id: process.env.BB_EMBEDDING_MODEL ?? "text-embedding-3-small" } }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
Provider2.list = list;
|
|
45
|
+
})(Provider || (Provider = {}));
|
|
46
|
+
|
|
47
|
+
// src/brain/semantic/embedding-config.ts
|
|
48
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
|
|
49
|
+
function intFromEnv(raw, fallback, min, max) {
|
|
50
|
+
if (raw === void 0 || raw === "") return fallback;
|
|
51
|
+
const n = Number(raw);
|
|
52
|
+
if (!Number.isFinite(n)) return fallback;
|
|
53
|
+
return Math.min(max, Math.max(min, Math.floor(n)));
|
|
54
|
+
}
|
|
55
|
+
function isEmbeddingDisabled(env = process.env) {
|
|
56
|
+
const raw = (env.BB_EMBEDDING_DISABLED ?? "").trim().toLowerCase();
|
|
57
|
+
if (raw === "") return false;
|
|
58
|
+
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
59
|
+
}
|
|
60
|
+
function readRuntimeOptions(env = process.env) {
|
|
61
|
+
const fbBase = (env.BB_EMBEDDING_FALLBACK_BASE_URL ?? "").trim();
|
|
62
|
+
const fbModel = (env.BB_EMBEDDING_FALLBACK_MODEL ?? "").trim();
|
|
63
|
+
const fbKey = (env.BB_EMBEDDING_FALLBACK_API_KEY ?? "").trim();
|
|
64
|
+
return {
|
|
65
|
+
disabled: isEmbeddingDisabled(env),
|
|
66
|
+
timeoutMs: intFromEnv(env.BB_EMBEDDING_TIMEOUT_MS, 3e4, 1e3, 6e5),
|
|
67
|
+
// 256, and the number is load-bearing rather than arbitrary.
|
|
68
|
+
//
|
|
69
|
+
// The indexer already chunks to `maxBatch = 100` snippets per call, so 256
|
|
70
|
+
// keeps every real indexing request a SINGLE request. An earlier default of
|
|
71
|
+
// 64 fragmented those 100-item batches into 64 + 36, and because a failure in
|
|
72
|
+
// any chunk discards the whole batch (SemanticEngine falls back to local for
|
|
73
|
+
// all of it, so a partial result is useless) the second request's failure
|
|
74
|
+
// threw away the first request's 64 good vectors. Measured: a mock that
|
|
75
|
+
// answered once and then 503ed stored 0 API vectors for 260 snippets under
|
|
76
|
+
// the 64 default, and all 260 under this one — the run no longer loses a
|
|
77
|
+
// completed request to an unrelated failure. 2048 is OpenAI's own input
|
|
78
|
+
// ceiling; staying well under it keeps self-hosted servers (llama.cpp caps
|
|
79
|
+
// far lower) working without configuration.
|
|
80
|
+
maxInputsPerRequest: intFromEnv(env.BB_EMBEDDING_MAX_INPUTS, 256, 1, 2048),
|
|
81
|
+
// Only the fallback provider retries inside the client. The PRIMARY provider's
|
|
82
|
+
// retry is the per-batch path in engine.ts (`_apiRetryAt` + per-batch degrade),
|
|
83
|
+
// which script/m30/verify-rate-limit.ts pins to EXACT call counts; a second
|
|
84
|
+
// client-side retry loop for the primary would double-retry and change those
|
|
85
|
+
// counts. See verifyNoteOnRetryOwnership below.
|
|
86
|
+
fallbackRetries: intFromEnv(env.BB_EMBEDDING_FALLBACK_RETRIES, 2, 0, 5),
|
|
87
|
+
retryBaseMs: intFromEnv(env.BB_EMBEDDING_RETRY_BASE_MS, 250, 10, 1e4),
|
|
88
|
+
retryCapMs: intFromEnv(env.BB_EMBEDDING_RETRY_CAP_MS, 4e3, 100, 6e4),
|
|
89
|
+
fallback: fbBase && fbModel ? { baseURL: fbBase.replace(/\/+$/, ""), model: fbModel, apiKey: fbKey } : null,
|
|
90
|
+
assertedDimension: readDimensionAssertion(env)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function readDimensionAssertion(env) {
|
|
94
|
+
const raw = (env.BB_EMBEDDING_DIMENSION ?? "").trim();
|
|
95
|
+
if (raw === "") return void 0;
|
|
96
|
+
const n = Number(raw);
|
|
97
|
+
if (!Number.isFinite(n) || n <= 0) return void 0;
|
|
98
|
+
return Math.floor(n);
|
|
99
|
+
}
|
|
100
|
+
function rawDimensionAssertion(env = process.env) {
|
|
101
|
+
return (env.BB_EMBEDDING_DIMENSION ?? "").trim();
|
|
102
|
+
}
|
|
103
|
+
function parseEndpoint(baseURL) {
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = new URL(baseURL);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
111
|
+
return { host: parsed.hostname, loopback: LOOPBACK_HOSTS.has(parsed.hostname.toLowerCase()) };
|
|
112
|
+
}
|
|
113
|
+
function assessEndpoint(baseURL, keySource, apiKeySource) {
|
|
114
|
+
const problems = [];
|
|
115
|
+
const parsed = parseEndpoint(baseURL);
|
|
116
|
+
const hasKey = keySource !== "none" && keySource !== "default";
|
|
117
|
+
if (!parsed) {
|
|
118
|
+
problems.push({
|
|
119
|
+
severity: "error",
|
|
120
|
+
envVar: "BB_EMBEDDING_BASE_URL",
|
|
121
|
+
message: `embedding base URL is not a usable http(s) endpoint (${JSON.stringify(baseURL)}). Set BB_EMBEDDING_BASE_URL to a full OpenAI-compatible /v1 URL, e.g. https://openrouter.ai/api/v1`
|
|
122
|
+
});
|
|
123
|
+
return { usable: false, loopback: false, hasKey, keySource, problems };
|
|
124
|
+
}
|
|
125
|
+
if (!parsed.loopback && !hasKey) {
|
|
126
|
+
problems.push({
|
|
127
|
+
severity: "error",
|
|
128
|
+
envVar: "BB_EMBEDDING_API_KEY",
|
|
129
|
+
message: `embedding provider ${parsed.host} is not local, so it needs a key, and none is set (source: ${apiKeySource}). Set BB_EMBEDDING_API_KEY, or point BB_EMBEDDING_BASE_URL at a loopback endpoint that needs no key. Until then every batch degrades to local word vectors.`
|
|
130
|
+
});
|
|
131
|
+
} else if (!parsed.loopback && hasKey && apiKeySource === "default") {
|
|
132
|
+
problems.push({
|
|
133
|
+
severity: "warning",
|
|
134
|
+
envVar: null,
|
|
135
|
+
message: "embedding key resolved from a config file; confirm that file is mode 0600 (bb doctor --fix)"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return { usable: true, loopback: parsed.loopback, hasKey, keySource, problems };
|
|
139
|
+
}
|
|
140
|
+
function describeEmbeddingSetup(eff, env = process.env) {
|
|
141
|
+
const options = readRuntimeOptions(env);
|
|
142
|
+
const problems = [];
|
|
143
|
+
if (options.disabled) {
|
|
144
|
+
return {
|
|
145
|
+
remoteReady: false,
|
|
146
|
+
disabled: true,
|
|
147
|
+
fallbackConfigured: options.fallback !== null,
|
|
148
|
+
problems: [
|
|
149
|
+
{
|
|
150
|
+
severity: "warning",
|
|
151
|
+
envVar: "BB_EMBEDDING_DISABLED",
|
|
152
|
+
message: "embeddings are disabled (BB_EMBEDDING_DISABLED). Indexing uses local word vectors; no embedding request will be made."
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (!eff.configured) {
|
|
158
|
+
const missingEnvVar = eff.sources.baseURL === "default" ? "BB_EMBEDDING_BASE_URL" : "BB_EMBEDDING_MODEL";
|
|
159
|
+
const sourceHint = eff.sources.baseURL === "default" && eff.sources.model === "default" ? "No embedding provider is configured, so semantic search uses local word vectors (50-dim, keyword-level). To enable a provider run `bb setup`, or set both BB_EMBEDDING_BASE_URL and BB_EMBEDDING_MODEL." : `Embedding provider is partially configured: ${missingEnvVar} is not set. Set ${missingEnvVar} (and BB_EMBEDDING_API_KEY for a hosted endpoint).`;
|
|
160
|
+
problems.push({ severity: "warning", envVar: missingEnvVar, message: sourceHint });
|
|
161
|
+
} else {
|
|
162
|
+
const readiness = assessEndpoint(eff.baseURL, eff.sources.apiKey, eff.sources.apiKey);
|
|
163
|
+
problems.push(...readiness.problems);
|
|
164
|
+
}
|
|
165
|
+
const rawDimension = rawDimensionAssertion(env);
|
|
166
|
+
if (rawDimension !== "" && options.assertedDimension === void 0) {
|
|
167
|
+
problems.push({
|
|
168
|
+
severity: "warning",
|
|
169
|
+
envVar: "BB_EMBEDDING_DIMENSION",
|
|
170
|
+
message: `BB_EMBEDDING_DIMENSION is set to ${JSON.stringify(rawDimension)}, which is not a positive integer. The assertion is ignored \u2014 vectors of any width will be accepted.`
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (options.fallback) {
|
|
174
|
+
const fb = assessEndpoint(options.fallback.baseURL, options.fallback.apiKey ? "env:BB_EMBEDDING_API_KEY" : "none", "env:BB_EMBEDDING_FALLBACK_API_KEY");
|
|
175
|
+
for (const problem of fb.problems) {
|
|
176
|
+
problems.push({
|
|
177
|
+
...problem,
|
|
178
|
+
message: `fallback provider: ${problem.message}`,
|
|
179
|
+
envVar: problem.envVar ? problem.envVar.replace("BB_EMBEDDING_API_KEY", "BB_EMBEDDING_FALLBACK_API_KEY").replace("BB_EMBEDDING_BASE_URL", "BB_EMBEDDING_FALLBACK_BASE_URL") : null
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (!options.fallback.model) {
|
|
183
|
+
problems.push({ severity: "error", envVar: "BB_EMBEDDING_FALLBACK_MODEL", message: "fallback provider is missing BB_EMBEDDING_FALLBACK_MODEL" });
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
const wantsFallback = (env.BB_EMBEDDING_FALLBACK_BASE_URL ?? "") !== "" || (env.BB_EMBEDDING_FALLBACK_MODEL ?? "") !== "";
|
|
187
|
+
if (wantsFallback) {
|
|
188
|
+
problems.push({
|
|
189
|
+
severity: "error",
|
|
190
|
+
envVar: wantsFallback && (env.BB_EMBEDDING_FALLBACK_BASE_URL ?? "") === "" ? "BB_EMBEDDING_FALLBACK_BASE_URL" : "BB_EMBEDDING_FALLBACK_MODEL",
|
|
191
|
+
message: "BB_EMBEDDING_FALLBACK_BASE_URL / BB_EMBEDDING_FALLBACK_MODEL are set inconsistently; both are required for the fallback provider"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
remoteReady: eff.configured && problems.every((p) => p.severity !== "error"),
|
|
197
|
+
disabled: false,
|
|
198
|
+
fallbackConfigured: options.fallback !== null,
|
|
199
|
+
problems
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function redactSecrets(text, ...secrets) {
|
|
203
|
+
let out = text;
|
|
204
|
+
for (const secret of secrets) {
|
|
205
|
+
if (typeof secret !== "string" || secret.length < 8) continue;
|
|
206
|
+
while (out.includes(secret)) out = out.replaceAll(secret, "[REDACTED]");
|
|
207
|
+
}
|
|
208
|
+
return out.replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi, "$1[REDACTED]");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/brain/semantic/openai-embed.ts
|
|
212
|
+
var log = create({ service: "openai-embed" });
|
|
213
|
+
function isEmbeddingConfigured(config) {
|
|
214
|
+
return resolveEmbeddingConfig(config).configured && !readRuntimeOptions().disabled;
|
|
215
|
+
}
|
|
216
|
+
async function getEmbeddingConfig() {
|
|
217
|
+
const config = await Config.get();
|
|
218
|
+
const eff = resolveEmbeddingConfig(config);
|
|
219
|
+
if (!eff.configured) return null;
|
|
220
|
+
const asserted = readRuntimeOptions().assertedDimension;
|
|
221
|
+
const cfg = {
|
|
222
|
+
baseURL: eff.baseURL,
|
|
223
|
+
apiKey: eff.apiKey,
|
|
224
|
+
model: eff.model,
|
|
225
|
+
dimension: asserted ?? (typeof config.embedding_dimension === "number" ? config.embedding_dimension : void 0)
|
|
226
|
+
};
|
|
227
|
+
if (!parseEndpoint(cfg.baseURL)) {
|
|
228
|
+
log.debug("embedding base URL is not a usable endpoint; remote embeddings off", { baseURL: cfg.baseURL });
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
return cfg;
|
|
232
|
+
}
|
|
233
|
+
var ADVICE = {
|
|
234
|
+
auth: "The provider rejected the API key. Set a valid key, or run `bb setup` to re-enter it. Nothing will be embedded until this is fixed.",
|
|
235
|
+
not_found: "The provider has no /embeddings endpoint at that base URL, or does not know the model name. Check BB_EMBEDDING_BASE_URL and BB_EMBEDDING_MODEL.",
|
|
236
|
+
rate_limit: "Rate limited. The key looks valid; indexing backs off and retries. On a free tier this is a quota, not a fault.",
|
|
237
|
+
server: "The provider returned a server error. Usually transient; indexing retries and falls back per batch meanwhile.",
|
|
238
|
+
timeout: "The provider did not respond in time. Check network reachability and the base URL.",
|
|
239
|
+
network: "Could not reach the provider. Check the base URL and network access.",
|
|
240
|
+
protocol: "The provider answered with something that is not an embeddings response. The base URL is probably not an OpenAI-compatible /v1 endpoint."
|
|
241
|
+
};
|
|
242
|
+
function classifyStatus(status) {
|
|
243
|
+
if (status === 401 || status === 403) return "auth";
|
|
244
|
+
if (status === 404) return "not_found";
|
|
245
|
+
if (status === 429) return "rate_limit";
|
|
246
|
+
if (status >= 500) return "server";
|
|
247
|
+
if (status >= 400) return "protocol";
|
|
248
|
+
return "server";
|
|
249
|
+
}
|
|
250
|
+
var EmbeddingApiError = class extends Error {
|
|
251
|
+
status;
|
|
252
|
+
retryAfterSec;
|
|
253
|
+
/** Actionable classification. `status === 429` keeps working unchanged. */
|
|
254
|
+
kind;
|
|
255
|
+
/** One line the user can act on. Never contains a key. */
|
|
256
|
+
advice;
|
|
257
|
+
/** Which provider produced it: "primary" | "fallback" | "probe". */
|
|
258
|
+
source;
|
|
259
|
+
/** Endpoint and model that failed — so a report can name them. Never a key. */
|
|
260
|
+
endpoint;
|
|
261
|
+
model;
|
|
262
|
+
constructor(status, message, retryAfterSec, opts = {}) {
|
|
263
|
+
super(message);
|
|
264
|
+
this.name = "EmbeddingApiError";
|
|
265
|
+
this.status = status;
|
|
266
|
+
this.retryAfterSec = retryAfterSec;
|
|
267
|
+
this.kind = opts.kind ?? (status === 0 ? "network" : classifyStatus(status));
|
|
268
|
+
this.advice = ADVICE[this.kind];
|
|
269
|
+
this.source = opts.source ?? "primary";
|
|
270
|
+
this.endpoint = opts.baseURL ?? "";
|
|
271
|
+
this.model = opts.model ?? "";
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
var authLatch = /* @__PURE__ */ new Map();
|
|
275
|
+
function observedEmbeddingFailures() {
|
|
276
|
+
return [...authLatch.values()].map((error) => ({
|
|
277
|
+
baseURL: error.endpoint,
|
|
278
|
+
model: error.model,
|
|
279
|
+
status: error.status,
|
|
280
|
+
kind: error.kind,
|
|
281
|
+
advice: error.advice,
|
|
282
|
+
message: error.message
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
function latchKey(baseURL, model) {
|
|
286
|
+
return `${baseURL}|${model}`;
|
|
287
|
+
}
|
|
288
|
+
async function probeEmbeddingEndpoint(cfg, optionsOverride) {
|
|
289
|
+
const started = Date.now();
|
|
290
|
+
const options = { ...readRuntimeOptions(), ...optionsOverride };
|
|
291
|
+
try {
|
|
292
|
+
const result = await attemptRequest(["bb doctor embedding probe"], cfg, options, "probe");
|
|
293
|
+
return { ok: true, elapsedMs: Date.now() - started, dimension: result.vectors[0]?.length };
|
|
294
|
+
} catch (e) {
|
|
295
|
+
const err = e?.name === "EmbeddingApiError" ? e : new EmbeddingApiError(0, String(e?.message ?? e), void 0, { kind: "network", source: "probe", baseURL: cfg.baseURL, model: cfg.model });
|
|
296
|
+
return {
|
|
297
|
+
ok: false,
|
|
298
|
+
elapsedMs: Date.now() - started,
|
|
299
|
+
status: err.status,
|
|
300
|
+
kind: err.kind,
|
|
301
|
+
advice: err.advice,
|
|
302
|
+
message: err.message
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function chunk(items, size) {
|
|
307
|
+
if (size <= 1 || items.length <= size) return [items];
|
|
308
|
+
const out = [];
|
|
309
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
function sleep(ms) {
|
|
313
|
+
return new Promise((resolve2) => {
|
|
314
|
+
const t = setTimeout(resolve2, ms);
|
|
315
|
+
if (typeof t === "object" && t && typeof t.unref === "function") t.unref();
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function backoffFor(attempt, opts, retryAfterSec) {
|
|
319
|
+
if (retryAfterSec && retryAfterSec > 0) {
|
|
320
|
+
return Math.min(retryAfterSec * 1e3, opts.retryCapMs) + Math.floor(Math.random() * 100);
|
|
321
|
+
}
|
|
322
|
+
const exponential = opts.retryBaseMs * 2 ** attempt;
|
|
323
|
+
const capped = Math.min(exponential, opts.retryCapMs);
|
|
324
|
+
return capped + Math.floor(Math.random() * Math.max(1, Math.floor(capped / 4)));
|
|
325
|
+
}
|
|
326
|
+
function isRetryable(error, attempt, maxAttempts) {
|
|
327
|
+
if (attempt >= maxAttempts - 1) return false;
|
|
328
|
+
return error.kind === "rate_limit" || error.kind === "server" || error.kind === "timeout" || error.kind === "network";
|
|
329
|
+
}
|
|
330
|
+
async function attemptRequest(texts, cfg, options, source) {
|
|
331
|
+
const who = { source, baseURL: cfg.baseURL, model: cfg.model };
|
|
332
|
+
const url = `${cfg.baseURL}/embeddings`;
|
|
333
|
+
const payload = JSON.stringify({
|
|
334
|
+
model: cfg.model,
|
|
335
|
+
input: texts.length === 1 ? texts[0] : texts,
|
|
336
|
+
encoding_format: "float"
|
|
337
|
+
});
|
|
338
|
+
const parsedUrl = new URL(url);
|
|
339
|
+
const endpoint = parseEndpoint(cfg.baseURL);
|
|
340
|
+
const isLocal = endpoint?.loopback ?? false;
|
|
341
|
+
if (isLocal) {
|
|
342
|
+
const loopbackHosts = ["127.0.0.1", "localhost", "::1"];
|
|
343
|
+
for (const key of ["NO_PROXY", "no_proxy"]) {
|
|
344
|
+
const existing = process.env[key] ?? "";
|
|
345
|
+
const values = new Set(existing.split(",").map((v) => v.trim()).filter(Boolean));
|
|
346
|
+
for (const host of loopbackHosts) values.add(host);
|
|
347
|
+
process.env[key] = [...values].join(",");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const controller = new AbortController();
|
|
351
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
352
|
+
let res;
|
|
353
|
+
try {
|
|
354
|
+
res = await fetch(url, {
|
|
355
|
+
method: "POST",
|
|
356
|
+
headers: {
|
|
357
|
+
"Content-Type": "application/json",
|
|
358
|
+
...cfg.apiKey ? { Authorization: `Bearer ${cfg.apiKey}` } : {}
|
|
359
|
+
},
|
|
360
|
+
body: payload,
|
|
361
|
+
signal: controller.signal
|
|
362
|
+
});
|
|
363
|
+
const status = res.status;
|
|
364
|
+
let retryAfterRaw = null;
|
|
365
|
+
if (status === 429 || status === 503) retryAfterRaw = res.headers.get("retry-after");
|
|
366
|
+
const body = await res.text();
|
|
367
|
+
clearTimeout(timer);
|
|
368
|
+
if (status < 200 || status >= 300) {
|
|
369
|
+
let retryAfterSec;
|
|
370
|
+
if (status === 429) {
|
|
371
|
+
const secs = retryAfterRaw ? Number(retryAfterRaw) : NaN;
|
|
372
|
+
if (Number.isFinite(secs) && secs > 0) retryAfterSec = secs;
|
|
373
|
+
}
|
|
374
|
+
throw new EmbeddingApiError(
|
|
375
|
+
status,
|
|
376
|
+
// Message text is part of the observed contract: script/m21/verify-embedding-provider.ts
|
|
377
|
+
// matches /embedding API 429/ on it. Redaction is added, not substituted.
|
|
378
|
+
`embedding API ${status}: ${redactSecrets(body.slice(0, 200), cfg.apiKey)}`,
|
|
379
|
+
retryAfterSec,
|
|
380
|
+
who
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
let data;
|
|
384
|
+
try {
|
|
385
|
+
data = JSON.parse(body);
|
|
386
|
+
} catch {
|
|
387
|
+
throw new EmbeddingApiError(status, `embedding API ${status}: response is not JSON \u2014 ${redactSecrets(body.slice(0, 200), cfg.apiKey)}`, void 0, { kind: "protocol", ...who });
|
|
388
|
+
}
|
|
389
|
+
if (!data || !Array.isArray(data.data)) {
|
|
390
|
+
throw new EmbeddingApiError(status, `embedding API ${status}: response has no data array`, void 0, { kind: "protocol", ...who });
|
|
391
|
+
}
|
|
392
|
+
const rows = [...data.data];
|
|
393
|
+
if (rows.some((row) => typeof row?.index === "number")) {
|
|
394
|
+
rows.sort((a, b) => (typeof a?.index === "number" ? a.index : 0) - (typeof b?.index === "number" ? b.index : 0));
|
|
395
|
+
}
|
|
396
|
+
const embeddings = rows.map((row) => row?.embedding);
|
|
397
|
+
if (embeddings.length !== texts.length) {
|
|
398
|
+
throw new Error(`embedding API returned ${embeddings.length} vectors for ${texts.length} inputs`);
|
|
399
|
+
}
|
|
400
|
+
return { vectors: validateVectors(embeddings, texts.length, cfg, source) };
|
|
401
|
+
} catch (e) {
|
|
402
|
+
clearTimeout(timer);
|
|
403
|
+
if (e?.name === "EmbeddingApiError") throw e;
|
|
404
|
+
if (e?.name === "AbortError") {
|
|
405
|
+
throw new EmbeddingApiError(0, `embedding provider did not respond within ${options.timeoutMs}ms`, void 0, { kind: "timeout", ...who });
|
|
406
|
+
}
|
|
407
|
+
throw new EmbeddingApiError(0, `embedding request failed: ${redactSecrets(String(e?.message ?? e).slice(0, 200), cfg.apiKey)}`, void 0, { kind: "network", ...who });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function validateVectors(embeddings, expected, cfg, source) {
|
|
411
|
+
const who = { source, baseURL: cfg.baseURL, model: cfg.model };
|
|
412
|
+
const out = [];
|
|
413
|
+
let dimension = null;
|
|
414
|
+
for (let i = 0; i < embeddings.length; i++) {
|
|
415
|
+
const vector = embeddings[i];
|
|
416
|
+
if (!Array.isArray(vector) || vector.length === 0) {
|
|
417
|
+
throw new EmbeddingApiError(200, `embedding API returned a non-vector for input ${i} of ${expected}`, void 0, { kind: "protocol", ...who });
|
|
418
|
+
}
|
|
419
|
+
for (const component of vector) {
|
|
420
|
+
if (typeof component !== "number" || !Number.isFinite(component)) {
|
|
421
|
+
throw new EmbeddingApiError(200, `embedding API returned a non-finite component in vector ${i}`, void 0, { kind: "protocol", ...who });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (dimension === null) dimension = vector.length;
|
|
425
|
+
else if (vector.length !== dimension) {
|
|
426
|
+
throw new EmbeddingApiError(
|
|
427
|
+
200,
|
|
428
|
+
`embedding API returned mixed dimensions (${dimension} and ${vector.length}) in one batch \u2014 these vectors cannot be stored together`,
|
|
429
|
+
void 0,
|
|
430
|
+
{ kind: "protocol", ...who }
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
out.push(vector);
|
|
434
|
+
}
|
|
435
|
+
if (cfg.dimension !== void 0 && dimension !== null && dimension !== cfg.dimension) {
|
|
436
|
+
throw new EmbeddingApiError(
|
|
437
|
+
200,
|
|
438
|
+
`embedding API returned ${dimension}-dimensional vectors but BB_EMBEDDING_DIMENSION declares ${cfg.dimension} \u2014 refusing to store vectors from a different space than the index already holds under "${cfg.model}". Reindex with the real dimension, or unset BB_EMBEDDING_DIMENSION.`,
|
|
439
|
+
void 0,
|
|
440
|
+
{ kind: "protocol", ...who }
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
return out;
|
|
444
|
+
}
|
|
445
|
+
async function embedOpenAICompatible(texts, cfg, optionsOverride) {
|
|
446
|
+
if (!Array.isArray(texts) || texts.length === 0) return [];
|
|
447
|
+
const options = optionsOverride ?? readRuntimeOptions();
|
|
448
|
+
if (options.disabled) {
|
|
449
|
+
throw new EmbeddingApiError(
|
|
450
|
+
0,
|
|
451
|
+
"embeddings are disabled (BB_EMBEDDING_DISABLED)",
|
|
452
|
+
void 0,
|
|
453
|
+
{ kind: "protocol", source: "primary", baseURL: cfg.baseURL, model: cfg.model }
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
const latched = authLatch.get(latchKey(cfg.baseURL, cfg.model));
|
|
457
|
+
if (latched) throw latched;
|
|
458
|
+
try {
|
|
459
|
+
return await runProvider(texts, cfg, options, "primary", 1);
|
|
460
|
+
} catch (primaryError) {
|
|
461
|
+
if (primaryError?.name !== "EmbeddingApiError") throw primaryError;
|
|
462
|
+
if (primaryError.kind === "auth" || primaryError.kind === "not_found") {
|
|
463
|
+
log.warn("embedding provider rejected the request; further calls skipped for this process", {
|
|
464
|
+
kind: primaryError.kind,
|
|
465
|
+
status: primaryError.status,
|
|
466
|
+
source: primaryError.source,
|
|
467
|
+
advice: primaryError.advice
|
|
468
|
+
});
|
|
469
|
+
authLatch.set(latchKey(cfg.baseURL, cfg.model), primaryError);
|
|
470
|
+
throw primaryError;
|
|
471
|
+
}
|
|
472
|
+
if (!options.fallback) throw primaryError;
|
|
473
|
+
log.debug("primary embedding provider failed; trying the configured fallback", {
|
|
474
|
+
kind: primaryError.kind,
|
|
475
|
+
status: primaryError.status
|
|
476
|
+
});
|
|
477
|
+
const fbCfg = {
|
|
478
|
+
baseURL: options.fallback.baseURL,
|
|
479
|
+
apiKey: options.fallback.apiKey,
|
|
480
|
+
model: options.fallback.model,
|
|
481
|
+
dimension: cfg.dimension
|
|
482
|
+
};
|
|
483
|
+
const fbLatched = authLatch.get(latchKey(fbCfg.baseURL, fbCfg.model));
|
|
484
|
+
if (fbLatched) throw fbLatched;
|
|
485
|
+
try {
|
|
486
|
+
const vectors = await runProvider(texts, fbCfg, options, "fallback", options.fallbackRetries + 1);
|
|
487
|
+
log.debug("fallback embedding provider served the batch", { model: fbCfg.model });
|
|
488
|
+
return vectors;
|
|
489
|
+
} catch (fallbackError) {
|
|
490
|
+
if (fallbackError?.name === "EmbeddingApiError" && (fallbackError.kind === "auth" || fallbackError.kind === "not_found")) {
|
|
491
|
+
log.warn("fallback embedding provider rejected the request; further calls skipped for this process", {
|
|
492
|
+
kind: fallbackError.kind,
|
|
493
|
+
status: fallbackError.status
|
|
494
|
+
});
|
|
495
|
+
authLatch.set(latchKey(fbCfg.baseURL, fbCfg.model), fallbackError);
|
|
496
|
+
}
|
|
497
|
+
throw fallbackError;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async function runProvider(texts, cfg, options, source, maxAttempts) {
|
|
502
|
+
const batches = chunk(texts, Math.max(1, options.maxInputsPerRequest));
|
|
503
|
+
const out = [];
|
|
504
|
+
for (let b = 0; b < batches.length; b++) {
|
|
505
|
+
const batch = batches[b];
|
|
506
|
+
let attempt = 0;
|
|
507
|
+
for (; ; ) {
|
|
508
|
+
try {
|
|
509
|
+
const result = await attemptRequest(batch, cfg, options, source);
|
|
510
|
+
out.push(...result.vectors);
|
|
511
|
+
break;
|
|
512
|
+
} catch (e) {
|
|
513
|
+
const err = e?.name === "EmbeddingApiError" ? e : new EmbeddingApiError(0, String(e?.message ?? e), void 0, { kind: "network", source });
|
|
514
|
+
if (!isRetryable(err, attempt, maxAttempts)) throw err;
|
|
515
|
+
const wait = backoffFor(attempt, options, err.retryAfterSec);
|
|
516
|
+
log.debug("embedding attempt failed; backing off before retry", {
|
|
517
|
+
source,
|
|
518
|
+
attempt: attempt + 1,
|
|
519
|
+
maxAttempts,
|
|
520
|
+
kind: err.kind,
|
|
521
|
+
status: err.status,
|
|
522
|
+
waitMs: wait
|
|
523
|
+
});
|
|
524
|
+
await sleep(wait);
|
|
525
|
+
attempt++;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return out;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/brain/semantic/engine.ts
|
|
533
|
+
var log2 = create({ service: "brain-semantic-engine" });
|
|
534
|
+
function loadConfigWorkerFallback() {
|
|
535
|
+
try {
|
|
536
|
+
const bbJsonPath = process.env.BB_CONFIG_FILE ? path.resolve(process.env.BB_CONFIG_FILE) : path.join(process.cwd(), "bb.json");
|
|
537
|
+
if (fs.existsSync(bbJsonPath)) {
|
|
538
|
+
return JSON.parse(fs.readFileSync(bbJsonPath, "utf8"));
|
|
539
|
+
}
|
|
540
|
+
} catch (_) {
|
|
541
|
+
}
|
|
542
|
+
return {};
|
|
543
|
+
}
|
|
544
|
+
var SemanticEngine;
|
|
545
|
+
((SemanticEngine2) => {
|
|
546
|
+
let _embeddingAvailable = null;
|
|
547
|
+
let _checked = false;
|
|
548
|
+
let _localAvailable = null;
|
|
549
|
+
let _lastModel = null;
|
|
550
|
+
SemanticEngine2.LOCAL_MODEL_LABEL = "local-word-vectors-v3";
|
|
551
|
+
function getLastEmbeddingModel() {
|
|
552
|
+
return _lastModel || SemanticEngine2.LOCAL_MODEL_LABEL;
|
|
553
|
+
}
|
|
554
|
+
SemanticEngine2.getLastEmbeddingModel = getLastEmbeddingModel;
|
|
555
|
+
async function isEmbeddingAvailable() {
|
|
556
|
+
if (_checked) return _embeddingAvailable === true;
|
|
557
|
+
_checked = true;
|
|
558
|
+
if (_localAvailable === null) {
|
|
559
|
+
try {
|
|
560
|
+
_localAvailable = await initLocalModel();
|
|
561
|
+
} catch {
|
|
562
|
+
_localAvailable = false;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (_localAvailable) {
|
|
566
|
+
_embeddingAvailable = true;
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
const config = await loadConfig();
|
|
571
|
+
if (!isEmbeddingConfigured(config)) {
|
|
572
|
+
_embeddingAvailable = false;
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
_embeddingAvailable = true;
|
|
576
|
+
return true;
|
|
577
|
+
} catch {
|
|
578
|
+
_embeddingAvailable = false;
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
SemanticEngine2.isEmbeddingAvailable = isEmbeddingAvailable;
|
|
583
|
+
async function getEmbeddingModel(configModel) {
|
|
584
|
+
if (configModel) return configModel;
|
|
585
|
+
try {
|
|
586
|
+
const cfg = await getEmbeddingConfig();
|
|
587
|
+
if (cfg) return cfg.model;
|
|
588
|
+
} catch {
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
const providers = await Provider.list();
|
|
592
|
+
const providerMap = providers;
|
|
593
|
+
if (providerMap["openai"]?.models?.["text-embedding-3-small"]) return "openai/text-embedding-3-small";
|
|
594
|
+
if (providerMap["azure"]?.models?.["text-embedding-3-small"]) return "azure/text-embedding-3-small";
|
|
595
|
+
if (providerMap["mistral"]?.models?.["mistral-embed"]) return "mistral/mistral-embed";
|
|
596
|
+
if (providerMap["google"]?.models?.["text-embedding-004"]) return "google/text-embedding-004";
|
|
597
|
+
if (providerMap["google-vertex"]?.models?.["text-embedding-004"]) return "google-vertex/text-embedding-004";
|
|
598
|
+
if (providerMap["cohere"]?.models?.["embed-english-v3.0"]) return "cohere/embed-english-v3.0";
|
|
599
|
+
if (providerMap["nvidia"]?.models?.["nvidia/nv-embedqa-e5-v5"]) return "nvidia/nvidia/nv-embedqa-e5-v5";
|
|
600
|
+
if (providerMap["cloudflare-workers-ai"]?.models?.["@cf/baai/bge-small-en-v1.5"])
|
|
601
|
+
return "cloudflare-workers-ai/@cf/baai/bge-small-en-v1.5";
|
|
602
|
+
if (providerMap["opencode"]?.models?.["text-embedding-3-small"]) return "opencode/text-embedding-3-small";
|
|
603
|
+
for (const p of Object.values(providerMap)) {
|
|
604
|
+
for (const m of Object.values(p.models || {})) {
|
|
605
|
+
if (m.id.toLowerCase().includes("embed")) return `${p.id}/${m.id}`;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
} catch (e) {
|
|
609
|
+
log2.debug("Failed to list providers for embedding model", { error: e });
|
|
610
|
+
}
|
|
611
|
+
log2.debug("No embedding model configured \u2014 defaulting to opencode/text-embedding-3-small");
|
|
612
|
+
return "opencode/text-embedding-3-small";
|
|
613
|
+
}
|
|
614
|
+
SemanticEngine2.getEmbeddingModel = getEmbeddingModel;
|
|
615
|
+
async function loadConfig() {
|
|
616
|
+
if (SemanticEngine2.__config) {
|
|
617
|
+
return SemanticEngine2.__config;
|
|
618
|
+
}
|
|
619
|
+
const bbJson = loadConfigWorkerFallback();
|
|
620
|
+
if (bbJson && (bbJson.embedding_model || bbJson.provider)) {
|
|
621
|
+
const eff = resolveEmbeddingConfig(bbJson);
|
|
622
|
+
const merged = { ...bbJson };
|
|
623
|
+
if (eff.configured) {
|
|
624
|
+
merged.embedding_base_url = eff.baseURL;
|
|
625
|
+
merged.embedding_model = eff.model;
|
|
626
|
+
merged.embedding_api_key = eff.apiKey;
|
|
627
|
+
}
|
|
628
|
+
return merged;
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
return await Config.get();
|
|
632
|
+
} catch (_) {
|
|
633
|
+
return bbJson;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const EMBED_CACHE_MAX = Number(process.env.BB_EMBEDDING_CACHE_MAX || 128);
|
|
637
|
+
const _embedCache = /* @__PURE__ */ new Map();
|
|
638
|
+
let _embedCacheHits = 0;
|
|
639
|
+
let _embedCacheMisses = 0;
|
|
640
|
+
function embedCacheKey(space, text) {
|
|
641
|
+
return `${space}\0${createHash("sha1").update(text).digest("hex")}`;
|
|
642
|
+
}
|
|
643
|
+
function embedCacheGet(key) {
|
|
644
|
+
const hit = _embedCache.get(key);
|
|
645
|
+
if (!hit) {
|
|
646
|
+
_embedCacheMisses++;
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
_embedCacheHits++;
|
|
650
|
+
_embedCache.delete(key);
|
|
651
|
+
_embedCache.set(key, hit);
|
|
652
|
+
return hit.slice();
|
|
653
|
+
}
|
|
654
|
+
function embedCachePut(key, vec) {
|
|
655
|
+
if (!Array.isArray(vec) || vec.length === 0) return;
|
|
656
|
+
if (!Number.isFinite(EMBED_CACHE_MAX) || EMBED_CACHE_MAX <= 0) return;
|
|
657
|
+
if (_embedCache.has(key)) _embedCache.delete(key);
|
|
658
|
+
_embedCache.set(key, vec.slice());
|
|
659
|
+
while (_embedCache.size > EMBED_CACHE_MAX) {
|
|
660
|
+
const oldest = _embedCache.keys().next().value;
|
|
661
|
+
if (oldest === void 0) break;
|
|
662
|
+
_embedCache.delete(oldest);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function embeddingCacheStats() {
|
|
666
|
+
return { size: _embedCache.size, max: EMBED_CACHE_MAX, hits: _embedCacheHits, misses: _embedCacheMisses };
|
|
667
|
+
}
|
|
668
|
+
SemanticEngine2.embeddingCacheStats = embeddingCacheStats;
|
|
669
|
+
function clearEmbeddingCache() {
|
|
670
|
+
_embedCache.clear();
|
|
671
|
+
_embedCacheHits = 0;
|
|
672
|
+
_embedCacheMisses = 0;
|
|
673
|
+
}
|
|
674
|
+
SemanticEngine2.clearEmbeddingCache = clearEmbeddingCache;
|
|
675
|
+
async function embed(text, projectId) {
|
|
676
|
+
if (_checked && !_embeddingAvailable) return [];
|
|
677
|
+
const config = await loadConfig();
|
|
678
|
+
if (isEmbeddingConfigured(config)) {
|
|
679
|
+
try {
|
|
680
|
+
const cfg = await getEmbeddingConfig();
|
|
681
|
+
if (cfg) {
|
|
682
|
+
const key = embedCacheKey(`${cfg.model}|${cfg.baseURL}`, text);
|
|
683
|
+
const cached = embedCacheGet(key);
|
|
684
|
+
if (cached) {
|
|
685
|
+
_embeddingAvailable = true;
|
|
686
|
+
_checked = true;
|
|
687
|
+
_lastModel = cfg.model;
|
|
688
|
+
return cached;
|
|
689
|
+
}
|
|
690
|
+
const res = await embedOpenAICompatible([text], cfg);
|
|
691
|
+
if (res.length > 0) {
|
|
692
|
+
_embeddingAvailable = true;
|
|
693
|
+
_checked = true;
|
|
694
|
+
_lastModel = cfg.model;
|
|
695
|
+
embedCachePut(key, res[0]);
|
|
696
|
+
return res[0];
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
} catch (e) {
|
|
700
|
+
log2.debug("API embedding failed", { error: e?.message?.slice(0, 80) });
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
if (_localAvailable === null) _localAvailable = await initLocalModel();
|
|
705
|
+
if (_localAvailable) {
|
|
706
|
+
const key = embedCacheKey(projectId ? `${SemanticEngine2.LOCAL_MODEL_LABEL}|${projectId}` : SemanticEngine2.LOCAL_MODEL_LABEL, text);
|
|
707
|
+
const cachedLocal = embedCacheGet(key);
|
|
708
|
+
if (cachedLocal) {
|
|
709
|
+
_embeddingAvailable = true;
|
|
710
|
+
_checked = true;
|
|
711
|
+
_lastModel = SemanticEngine2.LOCAL_MODEL_LABEL;
|
|
712
|
+
return cachedLocal;
|
|
713
|
+
}
|
|
714
|
+
const localResult = await embedLocal([text], await localVocabulary(projectId));
|
|
715
|
+
if (localResult.length > 0 && localResult[0].length > 0) {
|
|
716
|
+
_embeddingAvailable = true;
|
|
717
|
+
_checked = true;
|
|
718
|
+
_lastModel = SemanticEngine2.LOCAL_MODEL_LABEL;
|
|
719
|
+
embedCachePut(key, localResult[0]);
|
|
720
|
+
return localResult[0];
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
} catch {
|
|
724
|
+
}
|
|
725
|
+
return [];
|
|
726
|
+
}
|
|
727
|
+
SemanticEngine2.embed = embed;
|
|
728
|
+
async function localVocabulary(projectId) {
|
|
729
|
+
if (!projectId) return null;
|
|
730
|
+
try {
|
|
731
|
+
const { BrainDatabase } = await import("./database-TCTZDLE5.js");
|
|
732
|
+
const raw = await BrainDatabase.getRawInstance(projectId, { readonly: true });
|
|
733
|
+
return projectVocabulary(raw, String(projectId));
|
|
734
|
+
} catch {
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
let _apiRetryAt = 0;
|
|
739
|
+
SemanticEngine2.API_BACKOFF_CAP_MS = 3e4;
|
|
740
|
+
SemanticEngine2.API_BACKOFF_JITTER_MS = 500;
|
|
741
|
+
function backoffMs(retryAfterSec) {
|
|
742
|
+
const base = retryAfterSec && retryAfterSec > 0 ? retryAfterSec * 1e3 : 5e3;
|
|
743
|
+
const capped = Math.min(base, SemanticEngine2.API_BACKOFF_CAP_MS);
|
|
744
|
+
return capped + Math.floor(Math.random() * SemanticEngine2.API_BACKOFF_JITTER_MS);
|
|
745
|
+
}
|
|
746
|
+
async function embedMany(texts, projectId) {
|
|
747
|
+
if (_checked && !_embeddingAvailable) return [];
|
|
748
|
+
if (!texts || texts.length === 0) return [];
|
|
749
|
+
const config = await loadConfig();
|
|
750
|
+
if (Date.now() >= _apiRetryAt && isEmbeddingConfigured(config)) {
|
|
751
|
+
try {
|
|
752
|
+
const cfg = await getEmbeddingConfig();
|
|
753
|
+
if (cfg) {
|
|
754
|
+
const embeddings = await embedOpenAICompatible(texts, cfg);
|
|
755
|
+
if (embeddings.length > 0) {
|
|
756
|
+
_apiRetryAt = 0;
|
|
757
|
+
_embeddingAvailable = true;
|
|
758
|
+
_checked = true;
|
|
759
|
+
_lastModel = cfg.model;
|
|
760
|
+
return embeddings;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
} catch (e) {
|
|
764
|
+
if (e?.name === "EmbeddingApiError" && e?.status === 429) {
|
|
765
|
+
_apiRetryAt = Date.now() + backoffMs(e?.retryAfterSec);
|
|
766
|
+
log2.debug("API 429 \u2014 backing off before next attempt", { retryAfterSec: e?.retryAfterSec });
|
|
767
|
+
} else {
|
|
768
|
+
log2.debug("API embedding failed for this batch; next batch retries", { error: e?.message?.slice(0, 60) });
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
if (_localAvailable === null) _localAvailable = await initLocalModel();
|
|
774
|
+
if (_localAvailable) {
|
|
775
|
+
const vocabulary = await localVocabulary(projectId);
|
|
776
|
+
const localResult = await embedLocal(texts, vocabulary);
|
|
777
|
+
if (localResult.length > 0) {
|
|
778
|
+
_embeddingAvailable = true;
|
|
779
|
+
_checked = true;
|
|
780
|
+
_lastModel = SemanticEngine2.LOCAL_MODEL_LABEL;
|
|
781
|
+
return localResult;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
} catch (e) {
|
|
785
|
+
log2.debug("local embedding failed", { error: e?.message });
|
|
786
|
+
}
|
|
787
|
+
return [];
|
|
788
|
+
}
|
|
789
|
+
SemanticEngine2.embedMany = embedMany;
|
|
790
|
+
function cosineSimilarity(vecA, vecB) {
|
|
791
|
+
if (!vecA || !vecB || vecA.length !== vecB.length || vecA.length === 0) return 0;
|
|
792
|
+
let dotProduct = 0, normA = 0, normB = 0;
|
|
793
|
+
for (let i = 0; i < vecA.length; i++) {
|
|
794
|
+
dotProduct += vecA[i] * vecB[i];
|
|
795
|
+
normA += vecA[i] * vecA[i];
|
|
796
|
+
normB += vecB[i] * vecB[i];
|
|
797
|
+
}
|
|
798
|
+
if (normA === 0 || normB === 0) return 0;
|
|
799
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
800
|
+
}
|
|
801
|
+
SemanticEngine2.cosineSimilarity = cosineSimilarity;
|
|
802
|
+
function hashContent(text) {
|
|
803
|
+
return createHash("md5").update(text).digest("hex");
|
|
804
|
+
}
|
|
805
|
+
SemanticEngine2.hashContent = hashContent;
|
|
806
|
+
})(SemanticEngine || (SemanticEngine = {}));
|
|
807
|
+
|
|
808
|
+
export {
|
|
809
|
+
isEmbeddingDisabled,
|
|
810
|
+
readRuntimeOptions,
|
|
811
|
+
parseEndpoint,
|
|
812
|
+
assessEndpoint,
|
|
813
|
+
describeEmbeddingSetup,
|
|
814
|
+
classifyStatus,
|
|
815
|
+
observedEmbeddingFailures,
|
|
816
|
+
probeEmbeddingEndpoint,
|
|
817
|
+
SemanticEngine
|
|
818
|
+
};
|