@xdarkicex/openclaw-memory-libravdb 1.3.5

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 (80) hide show
  1. package/README.md +46 -0
  2. package/docs/README.md +14 -0
  3. package/docs/architecture-decisions/README.md +6 -0
  4. package/docs/architecture-decisions/adr-001-onnx-over-ollama.md +21 -0
  5. package/docs/architecture-decisions/adr-002-libravdb-over-lancedb.md +19 -0
  6. package/docs/architecture-decisions/adr-003-convex-gating-over-threshold.md +27 -0
  7. package/docs/architecture-decisions/adr-004-sidecar-over-native-ts.md +21 -0
  8. package/docs/architecture.md +188 -0
  9. package/docs/contributing.md +76 -0
  10. package/docs/dependencies.md +38 -0
  11. package/docs/embedding-profiles.md +42 -0
  12. package/docs/gating.md +329 -0
  13. package/docs/implementation.md +381 -0
  14. package/docs/installation.md +272 -0
  15. package/docs/mathematics.md +695 -0
  16. package/docs/models.md +63 -0
  17. package/docs/problem.md +64 -0
  18. package/docs/security.md +86 -0
  19. package/openclaw.plugin.json +84 -0
  20. package/package.json +41 -0
  21. package/scripts/build-sidecar.sh +30 -0
  22. package/scripts/postinstall.js +169 -0
  23. package/scripts/setup.sh +20 -0
  24. package/scripts/setup.ts +505 -0
  25. package/scripts/sidecar-release.d.ts +4 -0
  26. package/scripts/sidecar-release.js +17 -0
  27. package/sidecar/cmd/inspect_onnx/main.go +105 -0
  28. package/sidecar/compact/gate.go +273 -0
  29. package/sidecar/compact/gate_test.go +85 -0
  30. package/sidecar/compact/summarize.go +345 -0
  31. package/sidecar/compact/summarize_test.go +319 -0
  32. package/sidecar/compact/tokens.go +11 -0
  33. package/sidecar/config/config.go +119 -0
  34. package/sidecar/config/config_test.go +75 -0
  35. package/sidecar/embed/engine.go +696 -0
  36. package/sidecar/embed/engine_test.go +349 -0
  37. package/sidecar/embed/matryoshka.go +93 -0
  38. package/sidecar/embed/matryoshka_test.go +150 -0
  39. package/sidecar/embed/onnx_local.go +319 -0
  40. package/sidecar/embed/onnx_local_test.go +159 -0
  41. package/sidecar/embed/profile_contract_test.go +71 -0
  42. package/sidecar/embed/profile_eval_test.go +923 -0
  43. package/sidecar/embed/profiles.go +39 -0
  44. package/sidecar/go.mod +21 -0
  45. package/sidecar/go.sum +30 -0
  46. package/sidecar/health/check.go +33 -0
  47. package/sidecar/health/check_test.go +55 -0
  48. package/sidecar/main.go +151 -0
  49. package/sidecar/model/encoder.go +222 -0
  50. package/sidecar/model/registry.go +262 -0
  51. package/sidecar/model/registry_test.go +102 -0
  52. package/sidecar/model/seq2seq.go +133 -0
  53. package/sidecar/server/rpc.go +343 -0
  54. package/sidecar/server/rpc_test.go +350 -0
  55. package/sidecar/server/transport.go +160 -0
  56. package/sidecar/store/libravdb.go +676 -0
  57. package/sidecar/store/libravdb_test.go +472 -0
  58. package/sidecar/summarize/engine.go +360 -0
  59. package/sidecar/summarize/engine_test.go +148 -0
  60. package/sidecar/summarize/onnx_local.go +494 -0
  61. package/sidecar/summarize/onnx_local_test.go +48 -0
  62. package/sidecar/summarize/profiles.go +52 -0
  63. package/sidecar/summarize/tokenizer.go +13 -0
  64. package/sidecar/summarize/tokenizer_hf.go +76 -0
  65. package/sidecar/summarize/util.go +13 -0
  66. package/src/cli.ts +205 -0
  67. package/src/context-engine.ts +195 -0
  68. package/src/index.ts +27 -0
  69. package/src/memory-provider.ts +24 -0
  70. package/src/openclaw-plugin-sdk.d.ts +53 -0
  71. package/src/plugin-runtime.ts +67 -0
  72. package/src/recall-cache.ts +34 -0
  73. package/src/recall-utils.ts +22 -0
  74. package/src/rpc.ts +84 -0
  75. package/src/scoring.ts +58 -0
  76. package/src/sidecar.ts +506 -0
  77. package/src/tokens.ts +36 -0
  78. package/src/types.ts +146 -0
  79. package/tsconfig.json +20 -0
  80. package/tsconfig.tests.json +12 -0
@@ -0,0 +1,319 @@
1
+ package embed
2
+
3
+ import (
4
+ "crypto/sha256"
5
+ "encoding/hex"
6
+ "encoding/json"
7
+ "fmt"
8
+ "os"
9
+ "path/filepath"
10
+ "strings"
11
+
12
+ "github.com/sugarme/tokenizer"
13
+ "github.com/xDarkicex/openclaw-memory-libravdb/sidecar/model"
14
+ )
15
+
16
+ const defaultManifestName = "embedding.json"
17
+
18
+ type Profile struct {
19
+ Backend string `json:"backend"`
20
+ Family string `json:"family,omitempty"`
21
+ Dimensions int `json:"dimensions"`
22
+ Normalize bool `json:"normalize"`
23
+ MaxContextTokens int `json:"maxContextTokens,omitempty"`
24
+ ModelPath string `json:"modelPath,omitempty"`
25
+ Tokenizer string `json:"tokenizerPath,omitempty"`
26
+ Fingerprint string `json:"fingerprint"`
27
+ }
28
+
29
+ type embeddingManifest struct {
30
+ Backend string `json:"backend,omitempty"`
31
+ Profile string `json:"profile,omitempty"`
32
+ Family string `json:"family,omitempty"`
33
+ Model string `json:"model"`
34
+ Tokenizer string `json:"tokenizer,omitempty"`
35
+ Dimensions int `json:"dimensions"`
36
+ Normalize *bool `json:"normalize,omitempty"`
37
+ InputNames []string `json:"inputNames,omitempty"`
38
+ OutputName string `json:"outputName,omitempty"`
39
+ AddSpecialTokens *bool `json:"addSpecialTokens,omitempty"`
40
+ Pooling string `json:"pooling,omitempty"`
41
+ }
42
+
43
+ type onnxLocalSpec struct {
44
+ Backend string
45
+ Family string
46
+ RuntimePath string
47
+ ModelPath string
48
+ TokenizerPath string
49
+ Dimensions int
50
+ Normalize bool
51
+ InputNames []string
52
+ OutputName string
53
+ AddSpecialTokens bool
54
+ Pooling string
55
+ Profile Profile
56
+ }
57
+
58
+ type onnxLocalModel struct {
59
+ encoder *model.EncoderModel
60
+ }
61
+
62
+ var newONNXLocalBackend = func(spec onnxLocalSpec) (embeddingBackend, error) {
63
+ return newFileONNXBackend(spec)
64
+ }
65
+
66
+ func resolveONNXLocalSpec(cfg Config) (onnxLocalSpec, error) {
67
+ manifestPath, err := resolveManifestPath(cfg.ModelPath)
68
+ if err != nil {
69
+ return onnxLocalSpec{}, err
70
+ }
71
+
72
+ manifest, err := readManifest(manifestPath)
73
+ if err != nil {
74
+ return onnxLocalSpec{}, err
75
+ }
76
+ selectedProfile, hasProfile := lookupProfile(firstNonEmpty(cfg.Profile, manifest.Profile))
77
+
78
+ baseDir := filepath.Dir(manifestPath)
79
+ modelPath := resolveManifestAsset(baseDir, cfg.modelAssetOverride(), manifest.Model)
80
+ if modelPath == "" {
81
+ return onnxLocalSpec{}, fmt.Errorf("onnx-local manifest missing model path")
82
+ }
83
+
84
+ tokenizerPath := resolveManifestAsset(baseDir, cfg.tokenizerAssetOverride(), manifest.Tokenizer)
85
+ if tokenizerPath == "" {
86
+ return onnxLocalSpec{}, fmt.Errorf("onnx-local manifest missing tokenizer path")
87
+ }
88
+
89
+ if cfg.Dimensions > 0 && manifest.Dimensions > 0 && cfg.Dimensions != manifest.Dimensions {
90
+ return onnxLocalSpec{}, fmt.Errorf("onnx-local manifest dimensions %d do not match configured dimensions %d", manifest.Dimensions, cfg.Dimensions)
91
+ }
92
+
93
+ dimensions := manifest.Dimensions
94
+ if dimensions <= 0 && hasProfile {
95
+ dimensions = selectedProfile.Dimensions
96
+ }
97
+ if dimensions <= 0 {
98
+ dimensions = cfg.Dimensions
99
+ }
100
+ if dimensions <= 0 {
101
+ return onnxLocalSpec{}, fmt.Errorf("onnx-local dimensions must be configured in embedding.json or plugin config")
102
+ }
103
+
104
+ normalize := cfg.Normalize
105
+ if manifest.Normalize != nil {
106
+ normalize = *manifest.Normalize
107
+ } else if hasProfile {
108
+ normalize = selectedProfile.Normalize
109
+ }
110
+
111
+ inputNames := manifest.InputNames
112
+ if len(inputNames) == 0 {
113
+ inputNames = []string{"input_ids", "attention_mask", "token_type_ids"}
114
+ }
115
+
116
+ outputName := strings.TrimSpace(manifest.OutputName)
117
+ if outputName == "" {
118
+ outputName = "sentence_embedding"
119
+ }
120
+
121
+ addSpecialTokens := true
122
+ if manifest.AddSpecialTokens != nil {
123
+ addSpecialTokens = *manifest.AddSpecialTokens
124
+ }
125
+ pooling := strings.TrimSpace(manifest.Pooling)
126
+ if pooling == "" && outputName == "last_hidden_state" {
127
+ pooling = "mean"
128
+ }
129
+
130
+ profile := buildProfile(Profile{
131
+ Backend: "onnx-local",
132
+ Family: firstNonEmpty(strings.TrimSpace(manifest.Family), selectedProfile.Family, strings.TrimSpace(manifest.Backend), "onnx-local"),
133
+ Dimensions: dimensions,
134
+ Normalize: normalize,
135
+ MaxContextTokens: selectedProfile.MaxContextTokens,
136
+ ModelPath: modelPath,
137
+ Tokenizer: tokenizerPath,
138
+ })
139
+
140
+ return onnxLocalSpec{
141
+ Backend: "onnx-local",
142
+ Family: profile.Family,
143
+ RuntimePath: strings.TrimSpace(cfg.RuntimePath),
144
+ ModelPath: modelPath,
145
+ TokenizerPath: tokenizerPath,
146
+ Dimensions: dimensions,
147
+ Normalize: normalize,
148
+ InputNames: append([]string(nil), inputNames...),
149
+ OutputName: outputName,
150
+ AddSpecialTokens: addSpecialTokens,
151
+ Pooling: pooling,
152
+ Profile: profile,
153
+ }, nil
154
+ }
155
+
156
+ func newFileONNXBackend(spec onnxLocalSpec) (embeddingBackend, error) {
157
+ encoder, err := model.DefaultRegistry().LoadEncoder(model.EncoderSpec{
158
+ Key: spec.Profile.Fingerprint,
159
+ Profile: model.Profile{
160
+ Name: spec.Profile.Fingerprint,
161
+ Family: spec.Profile.Family,
162
+ Task: model.TaskEmbedding,
163
+ Dims: spec.Dimensions,
164
+ Normalize: spec.Normalize,
165
+ ModelPath: spec.ModelPath,
166
+ TokenizerPath: spec.TokenizerPath,
167
+ OrtLibPath: spec.RuntimePath,
168
+ },
169
+ RuntimePath: spec.RuntimePath,
170
+ ModelPath: spec.ModelPath,
171
+ TokenizerPath: spec.TokenizerPath,
172
+ InputNames: spec.InputNames,
173
+ OutputName: spec.OutputName,
174
+ Dimensions: spec.Dimensions,
175
+ AddSpecialTokens: spec.AddSpecialTokens,
176
+ Pooling: spec.Pooling,
177
+ })
178
+ if err != nil {
179
+ return nil, err
180
+ }
181
+
182
+ return miniLMBackend{
183
+ model: onnxLocalModel{
184
+ encoder: encoder,
185
+ },
186
+ normalize: spec.Normalize,
187
+ }, nil
188
+ }
189
+
190
+ func (m onnxLocalModel) Compute(sentence string, _ bool) ([]float32, error) {
191
+ if m.encoder == nil {
192
+ return nil, fmt.Errorf("encoder model not loaded")
193
+ }
194
+ return m.encoder.EmbedText(sentence)
195
+ }
196
+
197
+ func (m onnxLocalModel) TokenCount(sentence string, _ bool) (int, error) {
198
+ if m.encoder == nil {
199
+ return 0, fmt.Errorf("encoder model not loaded")
200
+ }
201
+ return m.encoder.TokenCount(sentence)
202
+ }
203
+
204
+ func (m onnxLocalModel) Encode(sentence string, _ bool) (*tokenizer.Encoding, error) {
205
+ if m.encoder == nil {
206
+ return nil, fmt.Errorf("encoder model not loaded")
207
+ }
208
+ return m.encoder.EncodeText(sentence, true)
209
+ }
210
+
211
+ func (m onnxLocalModel) ComputeEncoding(encoding tokenizer.Encoding) ([]float32, error) {
212
+ if m.encoder == nil {
213
+ return nil, fmt.Errorf("encoder model not loaded")
214
+ }
215
+ return m.encoder.EmbedEncoding(encoding)
216
+ }
217
+
218
+ func meanPoolLastHiddenState(flat []float32, attentionMask []int, seqLength int, dimensions int) []float32 {
219
+ vec := make([]float32, dimensions)
220
+ var denom float32
221
+ for tokenIdx := 0; tokenIdx < seqLength; tokenIdx++ {
222
+ if tokenIdx >= len(attentionMask) || attentionMask[tokenIdx] == 0 {
223
+ continue
224
+ }
225
+ denom++
226
+ base := tokenIdx * dimensions
227
+ for dim := 0; dim < dimensions; dim++ {
228
+ vec[dim] += flat[base+dim]
229
+ }
230
+ }
231
+ if denom == 0 {
232
+ return vec
233
+ }
234
+ for dim := range vec {
235
+ vec[dim] /= denom
236
+ }
237
+ return vec
238
+ }
239
+
240
+ func resolveManifestPath(modelPath string) (string, error) {
241
+ raw := strings.TrimSpace(modelPath)
242
+ if raw == "" {
243
+ return "", fmt.Errorf("onnx-local requires embeddingModelPath pointing to a model directory or embedding.json")
244
+ }
245
+
246
+ info, err := os.Stat(raw)
247
+ if err == nil && info.IsDir() {
248
+ return filepath.Join(raw, defaultManifestName), nil
249
+ }
250
+ if err == nil && strings.EqualFold(filepath.Base(raw), defaultManifestName) {
251
+ return raw, nil
252
+ }
253
+ if err == nil {
254
+ return filepath.Join(filepath.Dir(raw), defaultManifestName), nil
255
+ }
256
+ return "", err
257
+ }
258
+
259
+ func readManifest(path string) (embeddingManifest, error) {
260
+ data, err := os.ReadFile(path)
261
+ if err != nil {
262
+ return embeddingManifest{}, fmt.Errorf("failed to read %s: %w", path, err)
263
+ }
264
+ var manifest embeddingManifest
265
+ if err := json.Unmarshal(data, &manifest); err != nil {
266
+ return embeddingManifest{}, fmt.Errorf("failed to parse %s: %w", path, err)
267
+ }
268
+ return manifest, nil
269
+ }
270
+
271
+ func resolveManifestAsset(baseDir string, values ...string) string {
272
+ asset := firstNonEmpty(values...)
273
+ if asset == "" {
274
+ return ""
275
+ }
276
+ if filepath.IsAbs(asset) {
277
+ return asset
278
+ }
279
+ return filepath.Join(baseDir, asset)
280
+ }
281
+
282
+ func buildProfile(profile Profile) Profile {
283
+ hash := sha256.Sum256([]byte(strings.Join([]string{
284
+ profile.Backend,
285
+ profile.Family,
286
+ fmt.Sprintf("%d", profile.Dimensions),
287
+ fmt.Sprintf("%t", profile.Normalize),
288
+ fmt.Sprintf("%d", profile.MaxContextTokens),
289
+ profile.ModelPath,
290
+ profile.Tokenizer,
291
+ }, "\x00")))
292
+ profile.Fingerprint = hex.EncodeToString(hash[:])
293
+ return profile
294
+ }
295
+
296
+ func firstNonEmpty(values ...string) string {
297
+ for _, value := range values {
298
+ value = strings.TrimSpace(value)
299
+ if value != "" {
300
+ return value
301
+ }
302
+ }
303
+ return ""
304
+ }
305
+
306
+ func (cfg Config) modelAssetOverride() string {
307
+ modelPath := strings.TrimSpace(cfg.ModelPath)
308
+ if strings.EqualFold(filepath.Base(modelPath), defaultManifestName) {
309
+ return ""
310
+ }
311
+ if strings.HasSuffix(strings.ToLower(modelPath), ".onnx") {
312
+ return modelPath
313
+ }
314
+ return ""
315
+ }
316
+
317
+ func (cfg Config) tokenizerAssetOverride() string {
318
+ return strings.TrimSpace(cfg.TokenizerPath)
319
+ }
@@ -0,0 +1,159 @@
1
+ package embed
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+ )
9
+
10
+ type fakeONNXLocalBackend struct {
11
+ vector []float32
12
+ }
13
+
14
+ func (b fakeONNXLocalBackend) Embed(_ string, dimensions int) ([]float32, error) {
15
+ if len(b.vector) != dimensions {
16
+ return nil, nil
17
+ }
18
+ return append([]float32(nil), b.vector...), nil
19
+ }
20
+
21
+ func TestONNXLocalLoadsManifestDirectory(t *testing.T) {
22
+ dir := t.TempDir()
23
+ err := os.WriteFile(filepath.Join(dir, "embedding.json"), []byte(`{
24
+ "backend":"onnx-local",
25
+ "profile":"nomic-embed-text-v1.5",
26
+ "family":"nomic-embed-text-v1.5",
27
+ "model":"model.onnx",
28
+ "tokenizer":"tokenizer.json",
29
+ "dimensions":768,
30
+ "normalize":true,
31
+ "inputNames":["input_ids","attention_mask"],
32
+ "outputName":"sentence_embedding"
33
+ }`), 0o644)
34
+ if err != nil {
35
+ t.Fatalf("write manifest: %v", err)
36
+ }
37
+
38
+ original := newONNXLocalBackend
39
+ t.Cleanup(func() { newONNXLocalBackend = original })
40
+ newONNXLocalBackend = func(spec onnxLocalSpec) (embeddingBackend, error) {
41
+ if spec.Family != "nomic-embed-text-v1.5" {
42
+ t.Fatalf("unexpected family %q", spec.Family)
43
+ }
44
+ if !strings.HasSuffix(spec.ModelPath, "model.onnx") {
45
+ t.Fatalf("unexpected model path %q", spec.ModelPath)
46
+ }
47
+ if !strings.HasSuffix(spec.TokenizerPath, "tokenizer.json") {
48
+ t.Fatalf("unexpected tokenizer path %q", spec.TokenizerPath)
49
+ }
50
+ if spec.Dimensions != 768 {
51
+ t.Fatalf("unexpected dimensions %d", spec.Dimensions)
52
+ }
53
+ if len(spec.InputNames) != 2 {
54
+ t.Fatalf("unexpected input names %#v", spec.InputNames)
55
+ }
56
+ if spec.RuntimePath != "/opt/onnx/libonnxruntime.so" {
57
+ t.Fatalf("unexpected runtime path %q", spec.RuntimePath)
58
+ }
59
+ return fakeONNXLocalBackend{vector: make([]float32, 768)}, nil
60
+ }
61
+
62
+ engine := NewWithConfig(Config{
63
+ Backend: "onnx-local",
64
+ RuntimePath: "/opt/onnx/libonnxruntime.so",
65
+ ModelPath: dir,
66
+ Normalize: true,
67
+ })
68
+ if !engine.Ready() {
69
+ t.Fatalf("expected onnx-local engine to be ready, reason=%q", engine.Reason())
70
+ }
71
+ if engine.Mode() != "onnx-local" {
72
+ t.Fatalf("unexpected mode %q", engine.Mode())
73
+ }
74
+ if engine.Dimensions() != 768 {
75
+ t.Fatalf("expected manifest dimensions 768, got %d", engine.Dimensions())
76
+ }
77
+ if engine.Profile().Fingerprint == "" {
78
+ t.Fatalf("expected embedding fingerprint")
79
+ }
80
+ }
81
+
82
+ func TestONNXLocalRejectsManifestDimensionMismatch(t *testing.T) {
83
+ dir := t.TempDir()
84
+ err := os.WriteFile(filepath.Join(dir, "embedding.json"), []byte(`{
85
+ "model":"model.onnx",
86
+ "tokenizer":"tokenizer.json",
87
+ "dimensions":768
88
+ }`), 0o644)
89
+ if err != nil {
90
+ t.Fatalf("write manifest: %v", err)
91
+ }
92
+
93
+ original := newONNXLocalBackend
94
+ t.Cleanup(func() { newONNXLocalBackend = original })
95
+ newONNXLocalBackend = func(spec onnxLocalSpec) (embeddingBackend, error) {
96
+ return fakeONNXLocalBackend{vector: make([]float32, spec.Dimensions)}, nil
97
+ }
98
+
99
+ engine := NewWithConfig(Config{
100
+ Backend: "onnx-local",
101
+ RuntimePath: "/opt/onnx/libonnxruntime.so",
102
+ ModelPath: dir,
103
+ Dimensions: 384,
104
+ })
105
+ if engine.Ready() {
106
+ t.Fatalf("expected mismatched dimensions to fail readiness")
107
+ }
108
+ }
109
+
110
+ func TestONNXLocalCanUseShippedProfileDefaults(t *testing.T) {
111
+ dir := t.TempDir()
112
+ err := os.WriteFile(filepath.Join(dir, "embedding.json"), []byte(`{
113
+ "profile":"nomic-embed-text-v1.5",
114
+ "model":"model.onnx",
115
+ "tokenizer":"tokenizer.json"
116
+ }`), 0o644)
117
+ if err != nil {
118
+ t.Fatalf("write manifest: %v", err)
119
+ }
120
+
121
+ original := newONNXLocalBackend
122
+ t.Cleanup(func() { newONNXLocalBackend = original })
123
+ newONNXLocalBackend = func(spec onnxLocalSpec) (embeddingBackend, error) {
124
+ if spec.Dimensions != 768 {
125
+ t.Fatalf("expected profile dimensions 768, got %d", spec.Dimensions)
126
+ }
127
+ if spec.Family != "nomic-embed-text-v1.5" {
128
+ t.Fatalf("expected profile family, got %q", spec.Family)
129
+ }
130
+ return fakeONNXLocalBackend{vector: make([]float32, 768)}, nil
131
+ }
132
+
133
+ engine := NewWithConfig(Config{
134
+ Backend: "onnx-local",
135
+ RuntimePath: "/opt/onnx/libonnxruntime.so",
136
+ ModelPath: dir,
137
+ })
138
+ if !engine.Ready() {
139
+ t.Fatalf("expected profile-backed onnx-local engine to be ready, reason=%q", engine.Reason())
140
+ }
141
+ }
142
+
143
+ func TestMeanPoolLastHiddenStateUsesAttentionMask(t *testing.T) {
144
+ flat := []float32{
145
+ 1, 2,
146
+ 3, 4,
147
+ 100, 200,
148
+ }
149
+ mask := []int{1, 1, 0}
150
+
151
+ got := meanPoolLastHiddenState(flat, mask, 3, 2)
152
+
153
+ if len(got) != 2 {
154
+ t.Fatalf("unexpected pooled vector length %d", len(got))
155
+ }
156
+ if got[0] != 2 || got[1] != 3 {
157
+ t.Fatalf("unexpected pooled vector %#v", got)
158
+ }
159
+ }
@@ -0,0 +1,71 @@
1
+ package embed
2
+
3
+ import (
4
+ "encoding/json"
5
+ "os"
6
+ "path/filepath"
7
+ "testing"
8
+ )
9
+
10
+ type tokenizerContract struct {
11
+ Truncation *struct {
12
+ MaxLength int `json:"max_length"`
13
+ } `json:"truncation"`
14
+ }
15
+
16
+ func TestProfileMaxCtxMatchesTokenizerContract(t *testing.T) {
17
+ cases := []struct {
18
+ profile string
19
+ modelDir string
20
+ expectEnforced bool
21
+ expectedMax int
22
+ }{
23
+ {
24
+ profile: "all-minilm-l6-v2",
25
+ modelDir: filepath.Clean(filepath.Join("..", "..", ".models", "all-minilm-l6-v2")),
26
+ expectEnforced: true,
27
+ expectedMax: 128,
28
+ },
29
+ {
30
+ profile: "nomic-embed-text-v1.5",
31
+ modelDir: filepath.Clean(filepath.Join("..", "..", ".models", "nomic-embed-text-v1.5")),
32
+ expectEnforced: false,
33
+ expectedMax: 8192,
34
+ },
35
+ }
36
+
37
+ for _, tc := range cases {
38
+ t.Run(tc.profile, func(t *testing.T) {
39
+ profile, ok := lookupProfile(tc.profile)
40
+ if !ok {
41
+ t.Fatalf("profile %s not found", tc.profile)
42
+ }
43
+ if profile.MaxContextTokens != tc.expectedMax {
44
+ t.Fatalf("profile %s MaxContextTokens=%d want %d", tc.profile, profile.MaxContextTokens, tc.expectedMax)
45
+ }
46
+
47
+ data, err := os.ReadFile(filepath.Join(tc.modelDir, "tokenizer.json"))
48
+ if err != nil {
49
+ t.Fatalf("read tokenizer.json: %v", err)
50
+ }
51
+ var contract tokenizerContract
52
+ if err := json.Unmarshal(data, &contract); err != nil {
53
+ t.Fatalf("parse tokenizer.json: %v", err)
54
+ }
55
+
56
+ if tc.expectEnforced {
57
+ if contract.Truncation == nil {
58
+ t.Fatalf("expected tokenizer truncation contract for %s", tc.profile)
59
+ }
60
+ if contract.Truncation.MaxLength != tc.expectedMax {
61
+ t.Fatalf("profile %s MaxContextTokens=%d but tokenizer enforces %d", tc.profile, tc.expectedMax, contract.Truncation.MaxLength)
62
+ }
63
+ return
64
+ }
65
+
66
+ if contract.Truncation != nil {
67
+ t.Fatalf("expected tokenizer truncation to be disabled for %s, got max_length=%d", tc.profile, contract.Truncation.MaxLength)
68
+ }
69
+ })
70
+ }
71
+ }