@gmickel/gno 0.6.0 → 0.6.1

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.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Download policy resolution.
3
+ * Determines whether model downloads are allowed based on env/flags.
4
+ *
5
+ * @module src/llm/policy
6
+ */
7
+
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+ // Types
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+
12
+ export interface DownloadPolicy {
13
+ /** True if network is disabled (no HF API calls at all) */
14
+ offline: boolean;
15
+ /** True if auto-download is allowed (may still be blocked by offline) */
16
+ allowDownload: boolean;
17
+ }
18
+
19
+ export interface PolicyFlags {
20
+ /** --offline CLI flag */
21
+ offline?: boolean;
22
+ }
23
+
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+ // Helpers
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Check if env var is set (non-empty and truthy).
30
+ * Treats "1", "true", "yes" as truthy. Empty string or "0" as falsy.
31
+ */
32
+ export function envIsSet(
33
+ env: Record<string, string | undefined>,
34
+ key: string
35
+ ): boolean {
36
+ const val = env[key];
37
+ if (val === undefined || val === '') {
38
+ return false;
39
+ }
40
+ const lower = val.toLowerCase();
41
+ return lower === '1' || lower === 'true' || lower === 'yes';
42
+ }
43
+
44
+ // ─────────────────────────────────────────────────────────────────────────────
45
+ // Main
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+
48
+ /**
49
+ * Resolve download policy from environment and CLI flags.
50
+ *
51
+ * Precedence (first wins):
52
+ * 1. --offline flag → offline=true, allowDownload=false
53
+ * 2. HF_HUB_OFFLINE=1 → offline=true, allowDownload=false
54
+ * 3. GNO_OFFLINE=1 → offline=true, allowDownload=false
55
+ * 4. GNO_NO_AUTO_DOWNLOAD=1 → offline=false, allowDownload=false
56
+ * 5. Default → offline=false, allowDownload=true
57
+ */
58
+ export function resolveDownloadPolicy(
59
+ env: Record<string, string | undefined>,
60
+ flags: PolicyFlags
61
+ ): DownloadPolicy {
62
+ // 1. --offline flag takes highest precedence
63
+ if (flags.offline) {
64
+ return { offline: true, allowDownload: false };
65
+ }
66
+
67
+ // 2. HF_HUB_OFFLINE env var (standard HuggingFace offline mode)
68
+ if (envIsSet(env, 'HF_HUB_OFFLINE')) {
69
+ return { offline: true, allowDownload: false };
70
+ }
71
+
72
+ // 3. GNO_OFFLINE env var (GNO-specific offline mode)
73
+ if (envIsSet(env, 'GNO_OFFLINE')) {
74
+ return { offline: true, allowDownload: false };
75
+ }
76
+
77
+ // 4. GNO_NO_AUTO_DOWNLOAD env var (allow resolve but no download)
78
+ if (envIsSet(env, 'GNO_NO_AUTO_DOWNLOAD')) {
79
+ return { offline: false, allowDownload: false };
80
+ }
81
+
82
+ // 5. Default: allow downloads
83
+ return { offline: false, allowDownload: true };
84
+ }
@@ -6,7 +6,9 @@
6
6
 
7
7
  import { join as pathJoin } from 'node:path';
8
8
  import { parseUri } from '../../app/constants';
9
+ import { createNonTtyProgressRenderer } from '../../cli/progress';
9
10
  import { LlmAdapter } from '../../llm/nodeLlamaCpp/adapter';
11
+ import { resolveDownloadPolicy } from '../../llm/policy';
10
12
  import { getActivePreset } from '../../llm/registry';
11
13
  import type {
12
14
  EmbeddingPort,
@@ -128,6 +130,12 @@ export function handleQuery(
128
130
  const preset = getActivePreset(ctx.config);
129
131
  const llm = new LlmAdapter(ctx.config);
130
132
 
133
+ // Resolve download policy from env (MCP has no CLI flags)
134
+ const policy = resolveDownloadPolicy(process.env, {});
135
+
136
+ // Non-TTY progress for MCP (periodic lines to stderr, not \r)
137
+ const downloadProgress = createNonTtyProgressRenderer();
138
+
131
139
  let embedPort: EmbeddingPort | null = null;
132
140
  let genPort: GenerationPort | null = null;
133
141
  let rerankPort: RerankPort | null = null;
@@ -135,7 +143,10 @@ export function handleQuery(
135
143
 
136
144
  try {
137
145
  // Create embedding port (for vector search) - optional
138
- const embedResult = await llm.createEmbeddingPort(preset.embed);
146
+ const embedResult = await llm.createEmbeddingPort(preset.embed, {
147
+ policy,
148
+ onProgress: (progress) => downloadProgress('embed', progress),
149
+ });
139
150
  if (embedResult.ok) {
140
151
  embedPort = embedResult.value;
141
152
  }
@@ -164,7 +175,10 @@ export function handleQuery(
164
175
 
165
176
  // Create generation port (for expansion) - optional
166
177
  if (!noExpand) {
167
- const genResult = await llm.createGenerationPort(preset.gen);
178
+ const genResult = await llm.createGenerationPort(preset.gen, {
179
+ policy,
180
+ onProgress: (progress) => downloadProgress('gen', progress),
181
+ });
168
182
  if (genResult.ok) {
169
183
  genPort = genResult.value;
170
184
  }
@@ -172,7 +186,10 @@ export function handleQuery(
172
186
 
173
187
  // Create rerank port - optional
174
188
  if (!noRerank) {
175
- const rerankResult = await llm.createRerankPort(preset.rerank);
189
+ const rerankResult = await llm.createRerankPort(preset.rerank, {
190
+ policy,
191
+ onProgress: (progress) => downloadProgress('rerank', progress),
192
+ });
176
193
  if (rerankResult.ok) {
177
194
  rerankPort = rerankResult.value;
178
195
  }
@@ -6,7 +6,9 @@
6
6
 
7
7
  import { join as pathJoin } from 'node:path';
8
8
  import { parseUri } from '../../app/constants';
9
+ import { createNonTtyProgressRenderer } from '../../cli/progress';
9
10
  import { LlmAdapter } from '../../llm/nodeLlamaCpp/adapter';
11
+ import { resolveDownloadPolicy } from '../../llm/policy';
10
12
  import { getActivePreset } from '../../llm/registry';
11
13
  import { formatQueryForEmbedding } from '../../pipeline/contextual';
12
14
  import type { SearchResult, SearchResults } from '../../pipeline/types';
@@ -109,9 +111,18 @@ export function handleVsearch(
109
111
  const preset = getActivePreset(ctx.config);
110
112
  const modelUri = preset.embed;
111
113
 
114
+ // Resolve download policy from env (MCP has no CLI flags)
115
+ const policy = resolveDownloadPolicy(process.env, {});
116
+
117
+ // Non-TTY progress for MCP (periodic lines to stderr, not \r)
118
+ const downloadProgress = createNonTtyProgressRenderer();
119
+
112
120
  // Create LLM adapter for embeddings
113
121
  const llm = new LlmAdapter(ctx.config);
114
- const embedResult = await llm.createEmbeddingPort(modelUri);
122
+ const embedResult = await llm.createEmbeddingPort(modelUri, {
123
+ policy,
124
+ onProgress: (progress) => downloadProgress('embed', progress),
125
+ });
115
126
  if (!embedResult.ok) {
116
127
  throw new Error(
117
128
  `Failed to load embedding model: ${embedResult.error.message}. ` +
@@ -6,7 +6,9 @@
6
6
  */
7
7
 
8
8
  import type { Config } from '../config/types';
9
+ import type { CreatePortOptions } from '../llm/nodeLlamaCpp/adapter';
9
10
  import { LlmAdapter } from '../llm/nodeLlamaCpp/adapter';
11
+ import { resolveDownloadPolicy } from '../llm/policy';
10
12
  import { getActivePreset } from '../llm/registry';
11
13
  import type {
12
14
  DownloadProgress,
@@ -87,8 +89,27 @@ export async function createServerContext(
87
89
  const preset = getActivePreset(config);
88
90
  const llm = new LlmAdapter(config);
89
91
 
92
+ // Resolve download policy from env (serve has no CLI flags)
93
+ const policy = resolveDownloadPolicy(process.env, {});
94
+
95
+ // Progress callback updates downloadState for WebUI polling
96
+ const createPortOptions = (type: ModelType): CreatePortOptions => ({
97
+ policy,
98
+ onProgress: (progress) => {
99
+ downloadState.active = true;
100
+ downloadState.currentType = type;
101
+ downloadState.progress = progress;
102
+ if (progress.percent >= 100) {
103
+ downloadState.completed.push(type);
104
+ }
105
+ },
106
+ });
107
+
90
108
  // Try to create embedding port
91
- const embedResult = await llm.createEmbeddingPort(preset.embed);
109
+ const embedResult = await llm.createEmbeddingPort(
110
+ preset.embed,
111
+ createPortOptions('embed')
112
+ );
92
113
  if (embedResult.ok) {
93
114
  embedPort = embedResult.value;
94
115
  const initResult = await embedPort.init();
@@ -108,18 +129,30 @@ export async function createServerContext(
108
129
  }
109
130
 
110
131
  // Try to create generation port
111
- const genResult = await llm.createGenerationPort(preset.gen);
132
+ const genResult = await llm.createGenerationPort(
133
+ preset.gen,
134
+ createPortOptions('gen')
135
+ );
112
136
  if (genResult.ok) {
113
137
  genPort = genResult.value;
114
138
  console.log('AI answer generation enabled');
115
139
  }
116
140
 
117
141
  // Try to create rerank port
118
- const rerankResult = await llm.createRerankPort(preset.rerank);
142
+ const rerankResult = await llm.createRerankPort(
143
+ preset.rerank,
144
+ createPortOptions('rerank')
145
+ );
119
146
  if (rerankResult.ok) {
120
147
  rerankPort = rerankResult.value;
121
148
  console.log('Reranking enabled');
122
149
  }
150
+
151
+ // Reset download state after initialization
152
+ if (downloadState.active) {
153
+ downloadState.active = false;
154
+ downloadState.currentType = null;
155
+ }
123
156
  } catch (e) {
124
157
  // Log but don't fail - models are optional
125
158
  console.log(