@gmickel/gno 0.36.0 → 0.37.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.
@@ -131,6 +131,41 @@ async function writeOutput(
131
131
  }
132
132
  }
133
133
 
134
+ async function resolveTerminalLinkPolicy(
135
+ format: "terminal" | "json" | "files" | "csv" | "md" | "xml"
136
+ ): Promise<
137
+ | {
138
+ isTTY: boolean;
139
+ editorUriTemplate?: string | null;
140
+ }
141
+ | undefined
142
+ > {
143
+ if (format !== "terminal") {
144
+ return undefined;
145
+ }
146
+
147
+ const globals = getGlobals();
148
+ const envTemplate = process.env.GNO_EDITOR_URI_TEMPLATE?.trim();
149
+ if (envTemplate) {
150
+ return {
151
+ isTTY: process.stdout.isTTY ?? false,
152
+ editorUriTemplate: envTemplate,
153
+ };
154
+ }
155
+
156
+ const { loadConfig } = await import("../config");
157
+ const configResult = await loadConfig(globals.config);
158
+ const configTemplate = configResult.ok
159
+ ? configResult.value.editorUriTemplate?.trim()
160
+ : undefined;
161
+
162
+ return {
163
+ isTTY: process.stdout.isTTY ?? false,
164
+ editorUriTemplate:
165
+ configTemplate && configTemplate.length > 0 ? configTemplate : null,
166
+ };
167
+ }
168
+
134
169
  function parseCsvValues(raw: unknown): string[] | undefined {
135
170
  if (typeof raw !== "string") {
136
171
  return undefined;
@@ -317,6 +352,7 @@ function wireSearchCommands(program: Command): void {
317
352
  files: format === "files",
318
353
  full: Boolean(cmdOpts.full),
319
354
  lineNumbers: Boolean(cmdOpts.lineNumbers),
355
+ terminalLinks: await resolveTerminalLinkPolicy(format),
320
356
  });
321
357
  await writeOutput(output, format);
322
358
  });
@@ -425,6 +461,7 @@ function wireSearchCommands(program: Command): void {
425
461
  files: format === "files",
426
462
  full: Boolean(cmdOpts.full),
427
463
  lineNumbers: Boolean(cmdOpts.lineNumbers),
464
+ terminalLinks: await resolveTerminalLinkPolicy(format),
428
465
  });
429
466
  await writeOutput(output, format);
430
467
  });
@@ -594,6 +631,7 @@ function wireSearchCommands(program: Command): void {
594
631
  format,
595
632
  full: Boolean(cmdOpts.full),
596
633
  lineNumbers: Boolean(cmdOpts.lineNumbers),
634
+ terminalLinks: await resolveTerminalLinkPolicy(format),
597
635
  });
598
636
  await writeOutput(output, format);
599
637
  });
@@ -99,9 +99,20 @@ export const CollectionSchema = z.object({
99
99
  message: "Invalid BCP-47 language code (e.g., en, de, zh-CN, und)",
100
100
  })
101
101
  .optional(),
102
+
103
+ /** Optional per-collection model overrides */
104
+ models: z
105
+ .object({
106
+ embed: z.string().min(1).optional(),
107
+ rerank: z.string().min(1).optional(),
108
+ expand: z.string().min(1).optional(),
109
+ gen: z.string().min(1).optional(),
110
+ })
111
+ .optional(),
102
112
  });
103
113
 
104
114
  export type Collection = z.infer<typeof CollectionSchema>;
115
+ export type CollectionModelOverrides = NonNullable<Collection["models"]>;
105
116
 
106
117
  // ─────────────────────────────────────────────────────────────────────────────
107
118
  // Context Schema
@@ -245,6 +256,9 @@ export const ConfigSchema = z.object({
245
256
  /** FTS tokenizer (immutable after init) */
246
257
  ftsTokenizer: z.enum(FTS_TOKENIZERS).default(DEFAULT_FTS_TOKENIZER),
247
258
 
259
+ /** Optional terminal hyperlink editor URI template */
260
+ editorUriTemplate: z.string().min(1).optional(),
261
+
248
262
  /** Collection definitions */
249
263
  collections: z.array(CollectionSchema).default([]),
250
264
 
@@ -20,6 +20,15 @@ export interface MimeDetector {
20
20
  const EXTENSION_MAP: Record<string, string> = {
21
21
  ".md": "text/markdown",
22
22
  ".txt": "text/plain",
23
+ ".ts": "text/plain",
24
+ ".tsx": "text/plain",
25
+ ".js": "text/plain",
26
+ ".jsx": "text/plain",
27
+ ".py": "text/plain",
28
+ ".go": "text/plain",
29
+ ".rs": "text/plain",
30
+ ".swift": "text/plain",
31
+ ".c": "text/plain",
23
32
  ".pdf": "application/pdf",
24
33
  ".docx":
25
34
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@@ -21,6 +21,169 @@ const MAX_OVERLAP_PERCENT = 0.5;
21
21
 
22
22
  /** Regex for sentence ending followed by whitespace and capital letter (global) */
23
23
  const SENTENCE_END_REGEX = /[.!?](\s+)[A-Z]/g;
24
+ const MIN_CODE_CHUNK_PERCENT = 0.35;
25
+
26
+ type CodeChunkLanguage =
27
+ | "typescript"
28
+ | "tsx"
29
+ | "javascript"
30
+ | "jsx"
31
+ | "python"
32
+ | "go"
33
+ | "rust";
34
+
35
+ const CODE_CHUNK_MODE = "automatic";
36
+
37
+ const CODE_EXTENSION_MAP: Record<string, CodeChunkLanguage> = {
38
+ ".ts": "typescript",
39
+ ".tsx": "tsx",
40
+ ".js": "javascript",
41
+ ".jsx": "jsx",
42
+ ".py": "python",
43
+ ".go": "go",
44
+ ".rs": "rust",
45
+ };
46
+
47
+ const CODE_SUPPORTED_EXTENSIONS = Object.keys(CODE_EXTENSION_MAP);
48
+
49
+ const CODE_BREAKPOINT_PATTERNS: Record<CodeChunkLanguage, RegExp[]> = {
50
+ typescript: [
51
+ /^\s*import\s.+$/gm,
52
+ /^\s*export\s+(?:default\s+)?(?:class|function|interface|type|enum)\b.*$/gm,
53
+ /^\s*(?:export\s+)?(?:async\s+)?function\s+\w+/gm,
54
+ /^\s*(?:export\s+)?class\s+\w+/gm,
55
+ /^\s*(?:export\s+)?interface\s+\w+/gm,
56
+ /^\s*(?:export\s+)?type\s+\w+\s*=/gm,
57
+ /^\s*(?:export\s+)?enum\s+\w+/gm,
58
+ /^\s*(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/gm,
59
+ ],
60
+ tsx: [
61
+ /^\s*import\s.+$/gm,
62
+ /^\s*export\s+(?:default\s+)?(?:class|function|interface|type|enum)\b.*$/gm,
63
+ /^\s*(?:export\s+)?(?:async\s+)?function\s+\w+/gm,
64
+ /^\s*(?:export\s+)?class\s+\w+/gm,
65
+ /^\s*(?:export\s+)?interface\s+\w+/gm,
66
+ /^\s*(?:export\s+)?type\s+\w+\s*=/gm,
67
+ /^\s*(?:export\s+)?enum\s+\w+/gm,
68
+ /^\s*(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/gm,
69
+ ],
70
+ javascript: [
71
+ /^\s*import\s.+$/gm,
72
+ /^\s*export\s+(?:default\s+)?(?:class|function)\b.*$/gm,
73
+ /^\s*(?:export\s+)?(?:async\s+)?function\s+\w+/gm,
74
+ /^\s*(?:export\s+)?class\s+\w+/gm,
75
+ /^\s*(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/gm,
76
+ ],
77
+ jsx: [
78
+ /^\s*import\s.+$/gm,
79
+ /^\s*export\s+(?:default\s+)?(?:class|function)\b.*$/gm,
80
+ /^\s*(?:export\s+)?(?:async\s+)?function\s+\w+/gm,
81
+ /^\s*(?:export\s+)?class\s+\w+/gm,
82
+ /^\s*(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/gm,
83
+ ],
84
+ python: [
85
+ /^\s*(?:from|import)\s+\w+/gm,
86
+ /^\s*@[\w.]+/gm,
87
+ /^\s*(?:async\s+def|def|class)\s+\w+/gm,
88
+ ],
89
+ go: [/^\s*import\s+(?:\(|")/gm, /^\s*(?:func|type|const|var)\s+\w+/gm],
90
+ rust: [
91
+ /^\s*use\s+[A-Za-z0-9_:{}*, ]+;/gm,
92
+ /^\s*(?:pub\s+)?(?:fn|struct|enum|trait|impl)\b/gm,
93
+ ],
94
+ };
95
+
96
+ export interface CodeChunkingStatus {
97
+ mode: typeof CODE_CHUNK_MODE;
98
+ supportedExtensions: string[];
99
+ }
100
+
101
+ export function getCodeChunkingStatus(): CodeChunkingStatus {
102
+ return {
103
+ mode: CODE_CHUNK_MODE,
104
+ supportedExtensions: [...CODE_SUPPORTED_EXTENSIONS],
105
+ };
106
+ }
107
+
108
+ function detectCodeChunkLanguage(
109
+ sourcePath?: string
110
+ ): CodeChunkLanguage | null {
111
+ if (!sourcePath) {
112
+ return null;
113
+ }
114
+
115
+ const normalized = sourcePath.toLowerCase();
116
+ const matchedExtension = Object.keys(CODE_EXTENSION_MAP).find((extension) =>
117
+ normalized.endsWith(extension)
118
+ );
119
+
120
+ if (!matchedExtension) {
121
+ return null;
122
+ }
123
+
124
+ return CODE_EXTENSION_MAP[matchedExtension] ?? null;
125
+ }
126
+
127
+ function collectStructuralBreakPoints(
128
+ text: string,
129
+ sourcePath?: string
130
+ ): number[] {
131
+ const language = detectCodeChunkLanguage(sourcePath);
132
+ if (!language) {
133
+ return [];
134
+ }
135
+
136
+ const patterns = CODE_BREAKPOINT_PATTERNS[language];
137
+ if (!patterns) {
138
+ return [];
139
+ }
140
+
141
+ const points = new Set<number>();
142
+ for (const pattern of patterns) {
143
+ pattern.lastIndex = 0;
144
+ let match: RegExpExecArray | null = null;
145
+ while (true) {
146
+ match = pattern.exec(text);
147
+ if (!match) {
148
+ break;
149
+ }
150
+ if (match.index > 0) {
151
+ points.add(match.index);
152
+ }
153
+ }
154
+ }
155
+
156
+ return [...points].sort((a, b) => a - b);
157
+ }
158
+
159
+ function findStructuralBreakPoint(
160
+ breakPoints: number[],
161
+ currentPos: number,
162
+ target: number,
163
+ windowSize: number,
164
+ minChunkChars: number
165
+ ): number | null {
166
+ if (breakPoints.length === 0) {
167
+ return null;
168
+ }
169
+
170
+ const minStart = currentPos + minChunkChars;
171
+ const start = Math.max(minStart, target - windowSize);
172
+ const end = target + windowSize;
173
+ const candidates = breakPoints.filter(
174
+ (point) => point >= start && point <= end
175
+ );
176
+ if (candidates.length === 0) {
177
+ return null;
178
+ }
179
+
180
+ const beforeTarget = candidates.filter((point) => point <= target);
181
+ if (beforeTarget.length > 0) {
182
+ return beforeTarget.at(-1) ?? null;
183
+ }
184
+
185
+ return candidates[0] ?? null;
186
+ }
24
187
 
25
188
  /**
26
189
  * Line index for O(1) line number lookups.
@@ -160,7 +323,8 @@ export class MarkdownChunker implements ChunkerPort {
160
323
  chunk(
161
324
  markdown: string,
162
325
  params?: ChunkParams,
163
- documentLanguageHint?: string
326
+ documentLanguageHint?: string,
327
+ sourcePath?: string
164
328
  ): ChunkOutput[] {
165
329
  if (!markdown || markdown.trim().length === 0) {
166
330
  return [];
@@ -172,9 +336,14 @@ export class MarkdownChunker implements ChunkerPort {
172
336
  const maxChars = maxTokens * CHARS_PER_TOKEN;
173
337
  const overlapChars = Math.floor(maxChars * overlapPercent);
174
338
  const windowSize = Math.floor(maxChars * 0.1); // 10% window for break search
339
+ const minCodeChunkChars = Math.floor(maxChars * MIN_CODE_CHUNK_PERCENT);
175
340
 
176
341
  // Build line index once for O(log n) lookups
177
342
  const lineIndex = buildLineIndex(markdown);
343
+ const structuralBreakPoints = collectStructuralBreakPoints(
344
+ markdown,
345
+ sourcePath
346
+ );
178
347
 
179
348
  const chunks: ChunkOutput[] = [];
180
349
  let pos = 0;
@@ -185,12 +354,23 @@ export class MarkdownChunker implements ChunkerPort {
185
354
  const targetEnd = pos + maxChars;
186
355
 
187
356
  let endPos: number;
357
+ let usedStructuralBreak = false;
188
358
  if (targetEnd >= markdown.length) {
189
359
  // Last chunk - take rest
190
360
  endPos = markdown.length;
191
361
  } else {
192
- // Find a good break point
193
- endPos = findBreakPoint(markdown, targetEnd, windowSize);
362
+ const structuralBreakPoint = findStructuralBreakPoint(
363
+ structuralBreakPoints,
364
+ pos,
365
+ targetEnd,
366
+ windowSize,
367
+ minCodeChunkChars
368
+ );
369
+ usedStructuralBreak = structuralBreakPoint !== null;
370
+ endPos =
371
+ structuralBreakPoint ??
372
+ // Find a good prose break point
373
+ findBreakPoint(markdown, targetEnd, windowSize);
194
374
  }
195
375
 
196
376
  // Extract chunk text - preserve exactly (no trim!)
@@ -224,8 +404,9 @@ export class MarkdownChunker implements ChunkerPort {
224
404
  break;
225
405
  }
226
406
 
227
- // Calculate next position with overlap
228
- const nextPos = endPos - overlapChars;
407
+ // Structural chunks should begin on the detected boundary, not in the
408
+ // middle of the previous code block due to overlap backtracking.
409
+ const nextPos = usedStructuralBreak ? endPos : endPos - overlapChars;
229
410
  pos = Math.max(pos + 1, nextPos); // Ensure we always advance
230
411
  }
231
412
 
@@ -612,7 +612,8 @@ export class SyncService {
612
612
  const chunks = this.chunker.chunk(
613
613
  artifact.markdown,
614
614
  DEFAULT_CHUNK_PARAMS,
615
- artifact.languageHint ?? collection.languageHint
615
+ artifact.languageHint ?? collection.languageHint,
616
+ entry.relPath
616
617
  );
617
618
 
618
619
  // 10. Convert to ChunkInput for store
@@ -105,7 +105,8 @@ export interface ChunkerPort {
105
105
  chunk(
106
106
  markdown: string,
107
107
  params?: ChunkParams,
108
- documentLanguageHint?: string
108
+ documentLanguageHint?: string,
109
+ sourcePath?: string
109
110
  ): ChunkOutput[];
110
111
  }
111
112
 
@@ -5,7 +5,12 @@
5
5
  * @module src/llm/registry
6
6
  */
7
7
 
8
- import type { Config, ModelConfig, ModelPreset } from "../config/types";
8
+ import type {
9
+ CollectionModelOverrides,
10
+ Config,
11
+ ModelConfig,
12
+ ModelPreset,
13
+ } from "../config/types";
9
14
  import type { ModelType } from "./types";
10
15
 
11
16
  import { DEFAULT_MODEL_PRESETS } from "../config/types";
@@ -91,6 +96,16 @@ export function getAnswerModelUri(config: Config, override?: string): string {
91
96
  return preset.gen;
92
97
  }
93
98
 
99
+ export function getCollectionModelOverrides(
100
+ config: Config,
101
+ collection?: string
102
+ ): CollectionModelOverrides | undefined {
103
+ if (!collection) {
104
+ return undefined;
105
+ }
106
+ return config.collections.find((item) => item.name === collection)?.models;
107
+ }
108
+
94
109
  /**
95
110
  * Resolve a model URI for a given type.
96
111
  * Uses override if provided, otherwise from active preset.
@@ -98,11 +113,16 @@ export function getAnswerModelUri(config: Config, override?: string): string {
98
113
  export function resolveModelUri(
99
114
  config: Config,
100
115
  type: ModelType,
101
- override?: string
116
+ override?: string,
117
+ collection?: string
102
118
  ): string {
103
119
  if (override) {
104
120
  return override;
105
121
  }
122
+ const collectionModels = getCollectionModelOverrides(config, collection);
123
+ if (collectionModels?.[type]) {
124
+ return collectionModels[type];
125
+ }
106
126
  const preset = getActivePreset(config);
107
127
  if (type === "expand") {
108
128
  return preset.expand ?? preset.gen;
@@ -24,7 +24,7 @@ import { resolveDepthPolicy } from "../../core/depth-policy";
24
24
  import { normalizeStructuredQueryInput } from "../../core/structured-query";
25
25
  import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
26
26
  import { resolveDownloadPolicy } from "../../llm/policy";
27
- import { getActivePreset } from "../../llm/registry";
27
+ import { getActivePreset, resolveModelUri } from "../../llm/registry";
28
28
  import { type HybridSearchDeps, searchHybrid } from "../../pipeline/hybrid";
29
29
  import {
30
30
  createVectorIndexPort,
@@ -171,10 +171,16 @@ export function handleQuery(
171
171
  let expandPort: GenerationPort | null = null;
172
172
  let rerankPort: RerankPort | null = null;
173
173
  let vectorIndex: VectorIndexPort | null = null;
174
+ const embedUri = resolveModelUri(
175
+ ctx.config,
176
+ "embed",
177
+ undefined,
178
+ args.collection
179
+ );
174
180
 
175
181
  try {
176
182
  // Create embedding port (for vector search) - optional
177
- const embedResult = await llm.createEmbeddingPort(preset.embed, {
183
+ const embedResult = await llm.createEmbeddingPort(embedUri, {
178
184
  policy,
179
185
  onProgress: (progress) => downloadProgress("embed", progress),
180
186
  });
@@ -197,7 +203,7 @@ export function handleQuery(
197
203
  // Create expansion port - optional
198
204
  if (!noExpand && !hasStructuredModes) {
199
205
  const genResult = await llm.createExpansionPort(
200
- preset.expand ?? preset.gen,
206
+ resolveModelUri(ctx.config, "expand", undefined, args.collection),
201
207
  {
202
208
  policy,
203
209
  onProgress: (progress) => downloadProgress("expand", progress),
@@ -210,10 +216,13 @@ export function handleQuery(
210
216
 
211
217
  // Create rerank port - optional
212
218
  if (!noRerank) {
213
- const rerankResult = await llm.createRerankPort(preset.rerank, {
214
- policy,
215
- onProgress: (progress) => downloadProgress("rerank", progress),
216
- });
219
+ const rerankResult = await llm.createRerankPort(
220
+ resolveModelUri(ctx.config, "rerank", undefined, args.collection),
221
+ {
222
+ policy,
223
+ onProgress: (progress) => downloadProgress("rerank", progress),
224
+ }
225
+ );
217
226
  if (rerankResult.ok) {
218
227
  rerankPort = rerankResult.value;
219
228
  }
@@ -226,7 +235,7 @@ export function handleQuery(
226
235
  const dimensions = embedPort.dimensions();
227
236
  const db = ctx.store.getRawDb();
228
237
  const vectorResult = await createVectorIndexPort(db, {
229
- model: preset.embed,
238
+ model: embedUri,
230
239
  dimensions,
231
240
  });
232
241
  if (vectorResult.ok) {
@@ -13,7 +13,7 @@ import { parseUri } from "../../app/constants";
13
13
  import { createNonTtyProgressRenderer } from "../../cli/progress";
14
14
  import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
15
15
  import { resolveDownloadPolicy } from "../../llm/policy";
16
- import { getActivePreset } from "../../llm/registry";
16
+ import { resolveModelUri } from "../../llm/registry";
17
17
  import { formatQueryForEmbedding } from "../../pipeline/contextual";
18
18
  import {
19
19
  searchVectorWithEmbedding,
@@ -118,8 +118,12 @@ export function handleVsearch(
118
118
  }
119
119
 
120
120
  // Get model from active preset
121
- const preset = getActivePreset(ctx.config);
122
- const modelUri = preset.embed;
121
+ const modelUri = resolveModelUri(
122
+ ctx.config,
123
+ "embed",
124
+ undefined,
125
+ args.collection
126
+ );
123
127
 
124
128
  // Resolve download policy from env (MCP has no CLI flags)
125
129
  const policy = resolveDownloadPolicy(process.env, {});
package/src/sdk/client.ts CHANGED
@@ -61,6 +61,7 @@ import { defaultSyncService, type SyncResult } from "../ingestion";
61
61
  import { updateFrontmatterTags } from "../ingestion/frontmatter";
62
62
  import { LlmAdapter } from "../llm/nodeLlamaCpp/adapter";
63
63
  import { resolveDownloadPolicy } from "../llm/policy";
64
+ import { resolveModelUri } from "../llm/registry";
64
65
  import {
65
66
  generateGroundedAnswer,
66
67
  processAnswerResult,
@@ -209,6 +210,7 @@ class GnoClientImpl implements GnoClient {
209
210
  expand?: boolean;
210
211
  answer?: boolean;
211
212
  rerank?: boolean;
213
+ collection?: string;
212
214
  requiredEmbed?: boolean;
213
215
  requiredExpand?: boolean;
214
216
  requiredAnswer?: boolean;
@@ -228,7 +230,12 @@ class GnoClientImpl implements GnoClient {
228
230
 
229
231
  if (options.embed) {
230
232
  const embedResult = await this.llm.createEmbeddingPort(
231
- options.embedModel,
233
+ resolveModelUri(
234
+ this.config,
235
+ "embed",
236
+ options.embedModel,
237
+ options.collection
238
+ ),
232
239
  {
233
240
  policy: this.downloadPolicy,
234
241
  }
@@ -267,7 +274,12 @@ class GnoClientImpl implements GnoClient {
267
274
 
268
275
  if (options.expand) {
269
276
  const genResult = await this.llm.createExpansionPort(
270
- options.expandModel ?? options.genModel,
277
+ resolveModelUri(
278
+ this.config,
279
+ "expand",
280
+ options.expandModel ?? options.genModel,
281
+ options.collection
282
+ ),
271
283
  {
272
284
  policy: this.downloadPolicy,
273
285
  }
@@ -285,9 +297,17 @@ class GnoClientImpl implements GnoClient {
285
297
  }
286
298
 
287
299
  if (options.answer) {
288
- const genResult = await this.llm.createGenerationPort(options.genModel, {
289
- policy: this.downloadPolicy,
290
- });
300
+ const genResult = await this.llm.createGenerationPort(
301
+ resolveModelUri(
302
+ this.config,
303
+ "gen",
304
+ options.genModel,
305
+ options.collection
306
+ ),
307
+ {
308
+ policy: this.downloadPolicy,
309
+ }
310
+ );
291
311
  if (genResult.ok) {
292
312
  answerPort = genResult.value;
293
313
  } else if (options.requiredAnswer) {
@@ -305,7 +325,12 @@ class GnoClientImpl implements GnoClient {
305
325
 
306
326
  if (options.rerank) {
307
327
  const rerankResult = await this.llm.createRerankPort(
308
- options.rerankModel,
328
+ resolveModelUri(
329
+ this.config,
330
+ "rerank",
331
+ options.rerankModel,
332
+ options.collection
333
+ ),
309
334
  {
310
335
  policy: this.downloadPolicy,
311
336
  }
@@ -364,6 +389,7 @@ class GnoClientImpl implements GnoClient {
364
389
  embed: true,
365
390
  requiredEmbed: true,
366
391
  embedModel: options.model,
392
+ collection: options.collection,
367
393
  });
368
394
 
369
395
  try {
@@ -431,6 +457,7 @@ class GnoClientImpl implements GnoClient {
431
457
  expandModel: options.expandModel,
432
458
  genModel: options.genModel,
433
459
  rerankModel: options.rerankModel,
460
+ collection: options.collection,
434
461
  });
435
462
 
436
463
  try {
@@ -483,6 +510,7 @@ class GnoClientImpl implements GnoClient {
483
510
  genModel: options.genModel,
484
511
  embedModel: options.embedModel,
485
512
  rerankModel: options.rerankModel,
513
+ collection: options.collection,
486
514
  });
487
515
 
488
516
  try {
package/src/sdk/embed.ts CHANGED
@@ -19,7 +19,7 @@ import type {
19
19
  import type { GnoEmbedOptions, GnoEmbedResult } from "./types";
20
20
 
21
21
  import { embedBacklog } from "../embed";
22
- import { getActivePreset } from "../llm/registry";
22
+ import { resolveModelUri } from "../llm/registry";
23
23
  import { formatDocForEmbedding } from "../pipeline/contextual";
24
24
  import { err, ok } from "../store/types";
25
25
  import {
@@ -191,8 +191,12 @@ export async function runEmbed(
191
191
  const batchSize = options.batchSize ?? 32;
192
192
  const force = options.force ?? false;
193
193
  const dryRun = options.dryRun ?? false;
194
- const preset = getActivePreset(runtime.config);
195
- const modelUri = options.model ?? preset.embed;
194
+ const modelUri = resolveModelUri(
195
+ runtime.config,
196
+ "embed",
197
+ options.model,
198
+ options.collection
199
+ );
196
200
  const db = runtime.store.getRawDb();
197
201
  const stats: VectorStatsPort = createVectorStatsPort(db);
198
202
 
package/src/sdk/types.ts CHANGED
@@ -84,6 +84,7 @@ export interface GnoUpdateOptions {
84
84
  }
85
85
 
86
86
  export interface GnoEmbedOptions {
87
+ collection?: string;
87
88
  model?: string;
88
89
  batchSize?: number;
89
90
  force?: boolean;