@mikedown/core 0.1.0 → 0.3.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +183 -3
  3. package/dist/ai/engines-page.d.ts +130 -0
  4. package/dist/ai/engines-page.js +10 -0
  5. package/dist/ai/engines-page.js.map +1 -0
  6. package/dist/ai/index.d.ts +1531 -0
  7. package/dist/ai/index.js +395 -0
  8. package/dist/ai/index.js.map +1 -0
  9. package/dist/ai-ui-C7ENTWWU.js +3370 -0
  10. package/dist/ai-ui-C7ENTWWU.js.map +1 -0
  11. package/dist/chunk-7A5HEPLK.js +4910 -0
  12. package/dist/chunk-7A5HEPLK.js.map +1 -0
  13. package/dist/chunk-GWCRPPCX.js +18 -0
  14. package/dist/chunk-GWCRPPCX.js.map +1 -0
  15. package/dist/chunk-JZWKEBCI.js +3899 -0
  16. package/dist/chunk-JZWKEBCI.js.map +1 -0
  17. package/dist/chunk-L4FO63DF.js +113 -0
  18. package/dist/chunk-L4FO63DF.js.map +1 -0
  19. package/dist/chunk-LXZG7IKN.js +39 -0
  20. package/dist/chunk-LXZG7IKN.js.map +1 -0
  21. package/dist/chunk-QDVY7WW6.js +165 -0
  22. package/dist/chunk-QDVY7WW6.js.map +1 -0
  23. package/dist/chunk-QWKT57TR.js +62 -0
  24. package/dist/chunk-QWKT57TR.js.map +1 -0
  25. package/dist/chunk-TSP7BAV3.js +22 -0
  26. package/dist/chunk-TSP7BAV3.js.map +1 -0
  27. package/dist/index.d.ts +62 -441
  28. package/dist/index.js +6871 -1247
  29. package/dist/index.js.map +1 -1
  30. package/dist/schema-DYzxkuSb.d.ts +46 -0
  31. package/dist/shared/anchoring.js +4 -16
  32. package/dist/shared/anchoring.js.map +1 -1
  33. package/dist/shared/imagePaste.js +3 -57
  34. package/dist/shared/imagePaste.js.map +1 -1
  35. package/dist/shared/sha1.js +3 -57
  36. package/dist/shared/sha1.js.map +1 -1
  37. package/dist/shared/wordCount.js +4 -12
  38. package/dist/shared/wordCount.js.map +1 -1
  39. package/dist/styles.css +3700 -94
  40. package/dist/types-CgEeIUhi.d.ts +432 -0
  41. package/dist/types-CsVNVSNY.d.ts +597 -0
  42. package/package.json +17 -2
@@ -0,0 +1,3899 @@
1
+ import {
2
+ defaultAiSettings,
3
+ detectLocality,
4
+ engineLocality,
5
+ hostOfUrl,
6
+ normalizeTag
7
+ } from "./chunk-QDVY7WW6.js";
8
+
9
+ // src/ai/types.ts
10
+ var AI_ENGINE_KINDS = ["openai-compatible", "lmstudio", "ollama", "anthropic", "openai", "vscode-lm"];
11
+ var AI_HOST_ENGINE_KINDS = ["vscode-lm"];
12
+ var AI_CAPABILITY_FLAGS = ["chat", "stream", "vision", "json", "tools", "fim", "embed"];
13
+ var DEFAULT_CONTEXT_TOKENS = 8192;
14
+ var AI_FALLBACK_MODES = ["none", "default", "anyLocal", "any"];
15
+ var AI_ERROR_CODES = [
16
+ "unreachable",
17
+ "auth",
18
+ "modelNotFound",
19
+ "modelLoadFailed",
20
+ "contextOverflow",
21
+ "rateLimited",
22
+ "cloudBlocked",
23
+ "consentDenied",
24
+ "cancelled",
25
+ "badOutput",
26
+ "unknown"
27
+ ];
28
+
29
+ // src/ai/tasks.ts
30
+ var SUMMARIZE_MIN_CONTEXT_TOKENS = 32768;
31
+ var AI_TASKS = [
32
+ {
33
+ id: "rewrite",
34
+ label: "Rewrite",
35
+ description: "Selection rewrite, explain or expand, reading level, translation.",
36
+ requires: { chat: true, stream: true }
37
+ },
38
+ {
39
+ id: "continue",
40
+ label: "Continue",
41
+ description: "Inline ghost text. Prefers a small fill-in-the-middle model.",
42
+ requires: { stream: true },
43
+ requiresAny: ["fim", "chat"]
44
+ },
45
+ {
46
+ id: "chat",
47
+ label: "Chat",
48
+ description: "Chat with the document, prompt blocks.",
49
+ requires: { chat: true, stream: true }
50
+ },
51
+ {
52
+ id: "summarize",
53
+ label: "Summarize",
54
+ description: 'Summaries and "what changed". Works best with a large context window; long documents are cut and labelled.',
55
+ requires: { chat: true },
56
+ prefers: { contextTokens: SUMMARIZE_MIN_CONTEXT_TOKENS }
57
+ },
58
+ {
59
+ id: "structure",
60
+ label: "Structure",
61
+ description: "Frontmatter and tags, Mermaid, tables, code, titles, paste cleanup. Prefers a model with JSON output.",
62
+ requires: { chat: true },
63
+ prefers: { json: true }
64
+ },
65
+ {
66
+ id: "notes",
67
+ label: "Notes",
68
+ description: "Meeting notes to action items.",
69
+ requires: { chat: true, json: true }
70
+ },
71
+ {
72
+ id: "review",
73
+ label: "Review",
74
+ description: "Grammar pass and consistency checks.",
75
+ requires: { chat: true, json: true }
76
+ },
77
+ {
78
+ id: "vision",
79
+ label: "Vision",
80
+ description: "Alt text for images.",
81
+ requires: { vision: true }
82
+ },
83
+ {
84
+ id: "embed",
85
+ label: "Embeddings",
86
+ description: "Workspace search and link suggestions.",
87
+ requires: { embed: true },
88
+ hostMethod: "embed"
89
+ },
90
+ {
91
+ id: "transcribe",
92
+ label: "Transcribe",
93
+ description: "Dictation, through the host.",
94
+ requires: {},
95
+ hostMethod: "transcribe"
96
+ }
97
+ ];
98
+ function getAiTask(id) {
99
+ return AI_TASKS.find((t) => t.id === id);
100
+ }
101
+ function combineRequirements(a, b) {
102
+ const out = { ...a };
103
+ for (const [key, value] of Object.entries(b)) {
104
+ if (key === "contextTokens") {
105
+ if (typeof value === "number") {
106
+ out.contextTokens = Math.max(out.contextTokens ?? 0, value);
107
+ }
108
+ } else if (value === true) {
109
+ out[key] = true;
110
+ }
111
+ }
112
+ return out;
113
+ }
114
+ function missingCapabilities(caps, requires, requiresAny, defaultContextTokens = DEFAULT_CONTEXT_TOKENS) {
115
+ const missing = [];
116
+ for (const [key, value] of Object.entries(requires)) {
117
+ if (key === "contextTokens") {
118
+ if (typeof value === "number" && (caps.contextTokens ?? defaultContextTokens) < value) {
119
+ missing.push("contextTokens");
120
+ }
121
+ } else if (value === true && !caps[key]) {
122
+ missing.push(key);
123
+ }
124
+ }
125
+ if (requiresAny && requiresAny.length > 0 && !requiresAny.some((flag) => caps[flag])) {
126
+ missing.push(requiresAny[0]);
127
+ }
128
+ return missing;
129
+ }
130
+
131
+ // src/ai/features.ts
132
+ var AI_FEATURES = [
133
+ {
134
+ id: "rewrite",
135
+ label: "Rewrite selection",
136
+ description: "Shorter, longer, fix flow or a custom prompt, previewed before it replaces the selection.",
137
+ defaultEnabled: true,
138
+ defaultTask: "rewrite",
139
+ requires: { chat: true, stream: true },
140
+ status: "available",
141
+ sends: "selection"
142
+ },
143
+ {
144
+ id: "ghostText",
145
+ label: "Inline suggestions",
146
+ description: "Dim continuation text while you write. Sends the text around the cursor as you type.",
147
+ // Separate opt-in even with the master switch on (brainstorm idea 1).
148
+ defaultEnabled: false,
149
+ defaultTask: "continue",
150
+ requires: { stream: true },
151
+ status: "planned"
152
+ },
153
+ {
154
+ id: "chat",
155
+ label: "Chat with this document",
156
+ description: "A sidebar chat with the document as context.",
157
+ defaultEnabled: true,
158
+ defaultTask: "chat",
159
+ requires: { chat: true, stream: true },
160
+ status: "planned",
161
+ sends: "document"
162
+ },
163
+ {
164
+ id: "summary",
165
+ label: "Summary",
166
+ description: "Insert a summary or outline of the document.",
167
+ defaultEnabled: true,
168
+ defaultTask: "summarize",
169
+ requires: { chat: true },
170
+ status: "available",
171
+ sends: "document"
172
+ },
173
+ {
174
+ id: "frontmatter",
175
+ label: "Frontmatter and tags",
176
+ description: "Suggest a title, tags and other properties.",
177
+ defaultEnabled: true,
178
+ defaultTask: "structure",
179
+ requires: { chat: true },
180
+ status: "available",
181
+ sends: "documentAndTags"
182
+ },
183
+ {
184
+ id: "title",
185
+ label: "Suggest titles",
186
+ description: "Offer a few titles for the document.",
187
+ defaultEnabled: true,
188
+ defaultTask: "structure",
189
+ requires: { chat: true },
190
+ status: "available",
191
+ sends: "document"
192
+ },
193
+ {
194
+ id: "altText",
195
+ label: "Image alt text",
196
+ description: "Describe pasted images for their alt text.",
197
+ defaultEnabled: true,
198
+ defaultTask: "vision",
199
+ requires: { vision: true },
200
+ status: "planned"
201
+ }
202
+ ];
203
+ function getAiFeature(id) {
204
+ return AI_FEATURES.find((f) => f.id === id);
205
+ }
206
+ function resolveAiFeature(features, id) {
207
+ const definition = getAiFeature(id);
208
+ const saved = features[id] ?? {};
209
+ return {
210
+ id,
211
+ enabled: saved.enabled ?? definition?.defaultEnabled ?? false,
212
+ task: saved.task ?? definition?.defaultTask ?? id,
213
+ requires: definition?.requires ?? {},
214
+ definition
215
+ };
216
+ }
217
+
218
+ // src/ai/capabilityHeuristics.ts
219
+ var BASE_CAPABILITIES = {
220
+ chat: true,
221
+ stream: true,
222
+ vision: false,
223
+ json: false,
224
+ tools: false,
225
+ fim: false,
226
+ embed: false,
227
+ contextTokens: null
228
+ };
229
+ var NOT_A_CHAT_MODEL = { chat: false, stream: false, vision: false, json: false, tools: false, fim: false };
230
+ var FAMILY_RULES = [
231
+ { family: "qwen", pattern: /qwen/, caps: { json: true, tools: true, contextTokens: 32768 } },
232
+ { family: "qwen3", pattern: /qwen-?3(?![.\d])/, caps: { contextTokens: 40960 } },
233
+ // Qwen 3.5 and later (and the qwen4_exp preview) carry vision in the same weights.
234
+ { family: "qwen3.5+", pattern: /qwen-?3\.[5-9]|qwen-?4/, caps: { vision: true, contextTokens: 262144 } },
235
+ { family: "qwen-coder", pattern: /qwen.*coder/, caps: { vision: false, fim: true, contextTokens: 262144 } },
236
+ { family: "qwen2.5-coder", pattern: /qwen-?2\.5.*coder/, caps: { contextTokens: 32768 } },
237
+ { family: "gemma", pattern: /gemma/, caps: { json: true, contextTokens: 8192 } },
238
+ { family: "gemma3", pattern: /gemma-?3/, caps: { vision: true, contextTokens: 131072 } },
239
+ { family: "gemma4", pattern: /gemma-?4/, caps: { vision: true, tools: true, contextTokens: 131072 } },
240
+ { family: "codegemma", pattern: /codegemma/, caps: { vision: false, fim: true } },
241
+ { family: "llama3.1+", pattern: /llama-?3\.[1-3]/, caps: { json: true, tools: true, contextTokens: 131072 } },
242
+ { family: "llama4", pattern: /llama-?4/, caps: { json: true, tools: true, vision: true, contextTokens: 131072 } },
243
+ { family: "mistral", pattern: /mistral|mixtral|ministral|magistral/, caps: { json: true, tools: true, contextTokens: 32768 } },
244
+ { family: "mistral-vision", pattern: /mistral-small-3\.[1-9]|mistral-medium|magistral|pixtral/, caps: { vision: true, contextTokens: 131072 } },
245
+ { family: "codestral", pattern: /codestral/, caps: { fim: true, json: true, contextTokens: 32768 } },
246
+ { family: "deepseek", pattern: /deepseek/, caps: { json: true, tools: true, contextTokens: 131072 } },
247
+ { family: "deepseek-v4", pattern: /deepseek-?v4/, caps: { contextTokens: 1048576 } },
248
+ { family: "deepseek-coder", pattern: /deepseek-coder/, caps: { fim: true } },
249
+ { family: "minimax", pattern: /minimax/, caps: { json: true, tools: true, contextTokens: 196608 } },
250
+ { family: "glm", pattern: /glm/, caps: { json: true, tools: true, contextTokens: 131072 } },
251
+ { family: "glm5", pattern: /glm-?5/, caps: { contextTokens: 1048576 } },
252
+ { family: "gpt-oss", pattern: /gpt-oss/, caps: { json: true, tools: true, contextTokens: 131072 } },
253
+ { family: "phi4", pattern: /phi-?4/, caps: { json: true, contextTokens: 16384 } },
254
+ { family: "hermes", pattern: /hermes/, caps: { json: true, tools: true } },
255
+ { family: "nemotron", pattern: /nemotron/, caps: { json: true, tools: true, contextTokens: 131072 } },
256
+ { family: "starcoder", pattern: /starcoder|stable-?code/, caps: { fim: true } },
257
+ // Name markers for multimodal builds, whatever the family.
258
+ { family: "vision", pattern: /(^|[^a-z])vl([^a-z]|$)|vision|llava|minicpm-v|moondream|omni|multimodal/, caps: { vision: true } },
259
+ // Embedding models: not chat models at all.
260
+ { family: "embedding", pattern: /embed|(^|[/_-])(bge|gte|e5)-|minilm|mxbai|arctic-embed|snowflake/, caps: { ...NOT_A_CHAT_MODEL, embed: true, contextTokens: 8192 } },
261
+ { family: "bge", pattern: /(^|[/_-])bge-(small|base|large)/, caps: { contextTokens: 512 } },
262
+ // Speech and rerankers.
263
+ { family: "speech", pattern: /whisper|parakeet|kokoro|(^|[/_-])tts([/_-]|$)|rerank/, caps: { ...NOT_A_CHAT_MODEL, embed: false } }
264
+ ];
265
+ var CLAUDE_BASE = { chat: true, stream: true, vision: true, json: true, tools: true, fim: false, embed: false };
266
+ var ANTHROPIC_MODEL_TABLE = [
267
+ { prefix: "claude-", caps: { ...CLAUDE_BASE, contextTokens: 2e5 } },
268
+ { prefix: "claude-3", caps: { contextTokens: 2e5 } },
269
+ { prefix: "claude-haiku-4-5", caps: { contextTokens: 2e5 } },
270
+ { prefix: "claude-opus-4-6", caps: { contextTokens: 1e6 } },
271
+ { prefix: "claude-opus-4-7", caps: { contextTokens: 1e6 } },
272
+ { prefix: "claude-opus-4-8", caps: { contextTokens: 1e6 } },
273
+ { prefix: "claude-sonnet-4-6", caps: { contextTokens: 1e6 } },
274
+ { prefix: "claude-opus-5", caps: { contextTokens: 1e6 } },
275
+ { prefix: "claude-sonnet-5", caps: { contextTokens: 1e6 } },
276
+ { prefix: "claude-haiku-5", caps: { contextTokens: 1e6 } },
277
+ { prefix: "claude-fable-5", caps: { contextTokens: 1e6 } },
278
+ { prefix: "claude-mythos-5", caps: { contextTokens: 1e6 } }
279
+ ];
280
+ var OPENAI_MODEL_TABLE = [
281
+ { prefix: "gpt-", caps: { chat: true, stream: true, json: true, tools: true, vision: false, fim: false, embed: false, contextTokens: 128e3 } },
282
+ { prefix: "gpt-3.5", caps: { contextTokens: 16385 } },
283
+ { prefix: "gpt-4o", caps: { vision: true, contextTokens: 128e3 } },
284
+ { prefix: "gpt-4.1", caps: { vision: true, contextTokens: 1047576 } },
285
+ { prefix: "gpt-5", caps: { vision: true, contextTokens: 4e5 } },
286
+ { prefix: "gpt-oss", caps: { contextTokens: 131072 } },
287
+ { prefix: "o1", caps: { chat: true, stream: true, json: true, tools: true, vision: true, contextTokens: 2e5 } },
288
+ { prefix: "o3", caps: { chat: true, stream: true, json: true, tools: true, vision: true, contextTokens: 2e5 } },
289
+ { prefix: "o4", caps: { chat: true, stream: true, json: true, tools: true, vision: true, contextTokens: 2e5 } },
290
+ { prefix: "text-embedding-", caps: { ...NOT_A_CHAT_MODEL, embed: true, contextTokens: 8191 } },
291
+ { prefix: "whisper", caps: NOT_A_CHAT_MODEL },
292
+ { prefix: "tts-", caps: NOT_A_CHAT_MODEL },
293
+ { prefix: "gpt-4o-mini-tts", caps: NOT_A_CHAT_MODEL },
294
+ { prefix: "gpt-4o-transcribe", caps: NOT_A_CHAT_MODEL },
295
+ { prefix: "gpt-image", caps: NOT_A_CHAT_MODEL },
296
+ { prefix: "dall-e", caps: NOT_A_CHAT_MODEL },
297
+ { prefix: "omni-moderation", caps: NOT_A_CHAT_MODEL }
298
+ ];
299
+ var OPENAI_REASONING_ID = /^(o\d+|gpt-([5-9]|\d{2,})(\.\d+)?)(-|$)/;
300
+ function isOpenAiReasoningModel(modelId, kind) {
301
+ const id = bareOpenAiId(modelId, kind);
302
+ return id !== null && OPENAI_REASONING_ID.test(id);
303
+ }
304
+ function bareOpenAiId(modelId, kind) {
305
+ const id = modelId.trim().toLowerCase();
306
+ if (kind === "openai") {
307
+ return id.slice(id.lastIndexOf("/") + 1);
308
+ }
309
+ return id.startsWith("openai/") ? id.slice("openai/".length) : null;
310
+ }
311
+ var OPENAI_NONE_EFFORT_ID = /^gpt-5\.[1-9]\d*(-|$)/;
312
+ var OPENAI_MINIMAL_EFFORT_ID = /^gpt-5(-|$)/;
313
+ function openAiOffReasoningEffort(modelId, kind) {
314
+ const id = bareOpenAiId(modelId, kind);
315
+ if (id === null || !OPENAI_REASONING_ID.test(id)) {
316
+ return null;
317
+ }
318
+ if (/codex|-pro(-|$)/.test(id)) {
319
+ return "low";
320
+ }
321
+ if (OPENAI_NONE_EFFORT_ID.test(id)) {
322
+ return "none";
323
+ }
324
+ if (OPENAI_MINIMAL_EFFORT_ID.test(id)) {
325
+ return "minimal";
326
+ }
327
+ return "low";
328
+ }
329
+ function matchCloudTable(table, id) {
330
+ const rows = table.filter((r) => id.startsWith(r.prefix)).sort((a, b) => a.prefix.length - b.prefix.length);
331
+ if (rows.length === 0) {
332
+ return null;
333
+ }
334
+ return { caps: Object.assign({}, ...rows.map((r) => r.caps)), prefix: rows[rows.length - 1].prefix };
335
+ }
336
+ function detectCapabilities(modelId, kind) {
337
+ if (kind === "vscode-lm") {
338
+ return { capabilities: withDefaultContext({ ...BASE_CAPABILITIES }), family: "vscode-lm" };
339
+ }
340
+ const id = modelId.trim().toLowerCase();
341
+ const bare = id.slice(id.lastIndexOf("/") + 1);
342
+ const cloudTable = kind === "anthropic" || bare.startsWith("claude-") ? ANTHROPIC_MODEL_TABLE : kind === "openai" || /^(gpt-[\d.]|o[134](-|$)|text-embedding-|whisper|tts-|dall-e|gpt-image|omni-moderation)/.test(bare) ? OPENAI_MODEL_TABLE : null;
343
+ if (cloudTable) {
344
+ const hit = matchCloudTable(cloudTable, bare);
345
+ if (hit) {
346
+ return { capabilities: withDefaultContext({ ...BASE_CAPABILITIES, ...hit.caps }), family: hit.prefix };
347
+ }
348
+ }
349
+ let caps = { ...BASE_CAPABILITIES };
350
+ let family = null;
351
+ for (const rule of FAMILY_RULES) {
352
+ if (rule.pattern.test(id)) {
353
+ caps = { ...caps, ...rule.caps };
354
+ family = rule.family;
355
+ }
356
+ }
357
+ return { capabilities: withDefaultContext(caps), family };
358
+ }
359
+ function withDefaultContext(caps) {
360
+ return caps.contextTokens === null ? { ...caps, contextTokens: DEFAULT_CONTEXT_TOKENS } : caps;
361
+ }
362
+ function completeCapabilities(partial, modelId, kind) {
363
+ const out = { ...detectCapabilities(modelId, kind).capabilities };
364
+ for (const [key, value] of Object.entries(partial)) {
365
+ if (key === "contextTokens") {
366
+ if (typeof value === "number" && value > 0) {
367
+ out.contextTokens = value;
368
+ }
369
+ } else if (typeof value === "boolean" && key in out) {
370
+ out[key] = value;
371
+ }
372
+ }
373
+ return out;
374
+ }
375
+ function mergeDiscoveredModels(existing, discovered, kind, opts = {}) {
376
+ const addEnabled = opts.addEnabled ?? true;
377
+ const found = /* @__PURE__ */ new Map();
378
+ for (const d of discovered) {
379
+ if (!found.has(d.id)) {
380
+ found.set(d.id, d);
381
+ }
382
+ }
383
+ const merged = existing.map((model) => {
384
+ const d = found.get(model.id);
385
+ if (!d) {
386
+ return model.manual ? model : { ...model, missing: true };
387
+ }
388
+ const next = { ...model, label: model.label ?? d.label };
389
+ delete next.missing;
390
+ if (next.label === void 0) {
391
+ delete next.label;
392
+ }
393
+ if (model.capabilitiesSource !== "user") {
394
+ next.capabilities = completeCapabilities(d.capabilities, d.id, kind);
395
+ next.capabilitiesSource = d.source;
396
+ }
397
+ return next;
398
+ });
399
+ const known = new Set(existing.map((m) => m.id));
400
+ for (const d of found.values()) {
401
+ if (known.has(d.id)) {
402
+ continue;
403
+ }
404
+ const added = {
405
+ id: d.id,
406
+ enabled: addEnabled,
407
+ capabilities: completeCapabilities(d.capabilities, d.id, kind),
408
+ capabilitiesSource: d.source
409
+ };
410
+ if (d.label !== void 0) {
411
+ added.label = d.label;
412
+ }
413
+ merged.push(added);
414
+ }
415
+ return merged;
416
+ }
417
+
418
+ // src/ai/presets.ts
419
+ var AI_PRESET_FAMILIES = [
420
+ {
421
+ family: "local",
422
+ label: "Run on this machine",
423
+ explainer: "Your own model server, on this computer or your network. Text never leaves it."
424
+ },
425
+ {
426
+ family: "cloud",
427
+ label: "Cloud API key",
428
+ explainer: "Paste a key from the provider. It bills your account for what you use."
429
+ }
430
+ ];
431
+ var AI_HOST_PRESET_FAMILY = {
432
+ family: "host",
433
+ label: "Built into VS Code",
434
+ explainer: "A model VS Code already offers through your GitHub Copilot plan. No key to paste; text goes to GitHub\u2019s Copilot service."
435
+ };
436
+ var anthropicFamily = (word, label) => ({ label, match: new RegExp(`^claude-${word}-\\d`) });
437
+ var AI_ENGINE_PRESETS = [
438
+ {
439
+ id: "omlx",
440
+ name: "oMLX",
441
+ kind: "openai-compatible",
442
+ baseUrl: "http://127.0.0.1:8000/v1",
443
+ apiKeyEnv: "OMLX_API_KEY",
444
+ apiKeyFile: { path: "~/.omlx/settings.json", jsonPath: "auth.api_key" },
445
+ needsKey: true,
446
+ family: "local",
447
+ caption: "MLX models on Apple silicon",
448
+ keyHint: "oMLX keeps its key in ~/.omlx/settings.json, which is read automatically.",
449
+ installUrl: "https://omlx.ai"
450
+ },
451
+ {
452
+ id: "lmstudio",
453
+ name: "LM Studio",
454
+ kind: "lmstudio",
455
+ baseUrl: "http://127.0.0.1:1234/v1",
456
+ needsKey: false,
457
+ family: "local",
458
+ caption: "The LM Studio app\u2019s local server",
459
+ keyHint: "Only needed if you turned on authentication in LM Studio.",
460
+ installUrl: "https://lmstudio.ai/download",
461
+ detectPath: "/api/v0/models"
462
+ },
463
+ {
464
+ id: "ollama",
465
+ name: "Ollama",
466
+ kind: "ollama",
467
+ baseUrl: "http://127.0.0.1:11434/v1",
468
+ needsKey: false,
469
+ family: "local",
470
+ caption: "Models pulled with ollama",
471
+ keyHint: "Ollama needs no key.",
472
+ installUrl: "https://ollama.com/download",
473
+ detectPath: "/api/tags"
474
+ },
475
+ {
476
+ id: "llamacpp",
477
+ name: "llama.cpp server",
478
+ kind: "openai-compatible",
479
+ baseUrl: "http://127.0.0.1:8080/v1",
480
+ needsKey: false,
481
+ family: "local",
482
+ caption: "llama-server with a GGUF model",
483
+ keyHint: "Only needed if you started llama-server with --api-key.",
484
+ installUrl: "https://github.com/ggml-org/llama.cpp/blob/master/docs/install.md"
485
+ },
486
+ {
487
+ id: "vllm",
488
+ name: "vLLM",
489
+ kind: "openai-compatible",
490
+ baseUrl: "http://127.0.0.1:8000/v1",
491
+ apiKeyEnv: "VLLM_API_KEY",
492
+ needsKey: false,
493
+ family: "local",
494
+ caption: "vllm serve on a GPU box",
495
+ keyHint: "Only needed if you started vLLM with --api-key.",
496
+ installUrl: "https://docs.vllm.ai/en/latest/getting_started/installation/"
497
+ },
498
+ {
499
+ id: "custom",
500
+ name: "Custom OpenAI-compatible",
501
+ kind: "openai-compatible",
502
+ baseUrl: "http://127.0.0.1:8000/v1",
503
+ needsKey: false,
504
+ family: "local",
505
+ caption: "Any server that speaks the OpenAI API",
506
+ keyHint: "Leave blank if the server has no auth."
507
+ },
508
+ {
509
+ id: "anthropic",
510
+ name: "Anthropic",
511
+ kind: "anthropic",
512
+ baseUrl: "https://api.anthropic.com",
513
+ apiKeyEnv: "ANTHROPIC_API_KEY",
514
+ needsKey: true,
515
+ family: "cloud",
516
+ caption: "Claude models, API key from Anthropic",
517
+ keyHint: "Get an API key in the Claude Console",
518
+ keyUrl: "https://platform.claude.com/settings/keys",
519
+ pricingUrl: "https://platform.claude.com/docs/en/about-claude/pricing",
520
+ policy: { label: "Anthropic privacy and data use", url: "https://privacy.claude.com/en/collections/10663361-commercial-customers" },
521
+ popular: [anthropicFamily("sonnet", "Sonnet"), anthropicFamily("opus", "Opus"), anthropicFamily("haiku", "Haiku")]
522
+ },
523
+ {
524
+ id: "openai",
525
+ name: "OpenAI",
526
+ kind: "openai",
527
+ baseUrl: "https://api.openai.com/v1",
528
+ apiKeyEnv: "OPENAI_API_KEY",
529
+ needsKey: true,
530
+ family: "cloud",
531
+ caption: "GPT models, API key from OpenAI",
532
+ keyHint: "Get an API key on the OpenAI platform",
533
+ keyUrl: "https://platform.openai.com/api-keys",
534
+ pricingUrl: "https://developers.openai.com/api/docs/pricing",
535
+ policy: { label: "OpenAI API data controls", url: "https://developers.openai.com/api/docs/guides/your-data" },
536
+ popular: [
537
+ { label: "Flagship", match: /^gpt-\d+(\.\d+)?$/ },
538
+ { label: "Mini", match: /^gpt-\d+(\.\d+)?-mini$/ }
539
+ ]
540
+ },
541
+ {
542
+ id: "openrouter",
543
+ name: "OpenRouter",
544
+ kind: "openai-compatible",
545
+ baseUrl: "https://openrouter.ai/api/v1",
546
+ apiKeyEnv: "OPENROUTER_API_KEY",
547
+ needsKey: true,
548
+ family: "cloud",
549
+ caption: "Models from many providers with one key",
550
+ keyHint: "Get an API key on OpenRouter",
551
+ keyUrl: "https://openrouter.ai/keys",
552
+ pricingUrl: "https://openrouter.ai/pricing",
553
+ policy: {
554
+ label: "OpenRouter privacy policy",
555
+ url: "https://openrouter.ai/privacy",
556
+ note: "OpenRouter passes each request on to the provider that serves the model, whose own terms apply as well."
557
+ },
558
+ popular: [
559
+ { label: "Claude Sonnet", match: /^anthropic\/claude-sonnet-\d/ },
560
+ { label: "GPT", match: /^openai\/gpt-\d+(\.\d+)?$/ },
561
+ { label: "Gemini Pro", match: /^google\/gemini-\d+(\.\d+)?-pro$/ },
562
+ { label: "Gemini Flash", match: /^google\/gemini-\d+(\.\d+)?-flash$/ }
563
+ ]
564
+ },
565
+ {
566
+ id: "gemini",
567
+ name: "Google Gemini",
568
+ kind: "openai-compatible",
569
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
570
+ apiKeyEnv: "GEMINI_API_KEY",
571
+ needsKey: true,
572
+ family: "cloud",
573
+ caption: "Gemini models, API key from Google AI Studio",
574
+ keyHint: "Get an API key in Google AI Studio",
575
+ keyUrl: "https://aistudio.google.com/apikey",
576
+ pricingUrl: "https://ai.google.dev/gemini-api/docs/pricing",
577
+ policy: { label: "Gemini API terms and data use", url: "https://ai.google.dev/gemini-api/terms" },
578
+ popular: [
579
+ { label: "Flash", match: /(^|\/)gemini-\d+(\.\d+)?-flash$/ },
580
+ { label: "Pro", match: /(^|\/)gemini-\d+(\.\d+)?-pro$/ },
581
+ { label: "Flash-Lite", match: /(^|\/)gemini-\d+(\.\d+)?-flash-lite$/ }
582
+ ]
583
+ },
584
+ {
585
+ id: "groq",
586
+ name: "Groq",
587
+ kind: "openai-compatible",
588
+ baseUrl: "https://api.groq.com/openai/v1",
589
+ apiKeyEnv: "GROQ_API_KEY",
590
+ needsKey: true,
591
+ family: "cloud",
592
+ caption: "Fast open models such as Llama and Qwen",
593
+ keyHint: "Get an API key in the Groq console",
594
+ keyUrl: "https://console.groq.com/keys",
595
+ pricingUrl: "https://console.groq.com/docs/models",
596
+ policy: { label: "Groq: your data in GroqCloud", url: "https://console.groq.com/docs/your-data" },
597
+ popular: [
598
+ { label: "GPT-OSS", match: /^openai\/gpt-oss-\d+b$/ },
599
+ { label: "Llama", match: /^(meta-llama\/)?llama-\d/, exclude: /guard|prompt/ },
600
+ { label: "Qwen", match: /^qwen\/qwen\d/ }
601
+ ]
602
+ },
603
+ {
604
+ id: "mistral",
605
+ name: "Mistral",
606
+ kind: "openai-compatible",
607
+ baseUrl: "https://api.mistral.ai/v1",
608
+ apiKeyEnv: "MISTRAL_API_KEY",
609
+ needsKey: true,
610
+ family: "cloud",
611
+ caption: "Mistral models, API key from Mistral",
612
+ keyHint: "Get an API key in the Mistral console",
613
+ keyUrl: "https://console.mistral.ai/api-keys",
614
+ pricingUrl: "https://mistral.ai/pricing",
615
+ policy: { label: "Mistral AI privacy policy", url: "https://legal.mistral.ai/terms/privacy-policy/" },
616
+ popular: [
617
+ { label: "Medium", match: /^mistral-medium-/, prefer: /-latest$/ },
618
+ { label: "Large", match: /^mistral-large-/, prefer: /-latest$/ },
619
+ { label: "Small", match: /^mistral-small-/, prefer: /-latest$/ }
620
+ ]
621
+ },
622
+ {
623
+ id: "deepseek",
624
+ name: "DeepSeek",
625
+ kind: "openai-compatible",
626
+ baseUrl: "https://api.deepseek.com/v1",
627
+ apiKeyEnv: "DEEPSEEK_API_KEY",
628
+ needsKey: true,
629
+ family: "cloud",
630
+ caption: "DeepSeek chat and reasoning models",
631
+ keyHint: "Get an API key on the DeepSeek platform",
632
+ keyUrl: "https://platform.deepseek.com/api_keys",
633
+ pricingUrl: "https://api-docs.deepseek.com/quick_start/pricing",
634
+ policy: { label: "DeepSeek privacy policy", url: "https://cdn.deepseek.com/policies/en-US/deepseek-privacy-policy.html" },
635
+ popular: [
636
+ { label: "Chat", match: /^deepseek-chat/ },
637
+ { label: "Reasoner", match: /^deepseek-reasoner/ }
638
+ ]
639
+ },
640
+ {
641
+ id: "vscodelm",
642
+ name: "GitHub Copilot",
643
+ kind: "vscode-lm",
644
+ baseUrl: "",
645
+ needsKey: false,
646
+ family: "host",
647
+ caption: "Copilot\u2019s models in VS Code, no key needed",
648
+ keyHint: "No key: VS Code signs in to GitHub Copilot for you. Needs a Copilot plan and the GitHub Copilot Chat extension.",
649
+ pricingUrl: "https://github.com/features/copilot/plans",
650
+ policy: {
651
+ label: "GitHub Copilot Trust Center",
652
+ url: "https://copilot.github.trust.page/",
653
+ note: "Text goes to GitHub\u2019s Copilot service under your Copilot plan; individual and business plans handle data differently."
654
+ }
655
+ }
656
+ ];
657
+ function urlKey(url) {
658
+ return url.trim().toLowerCase().replace(/\/+$/, "");
659
+ }
660
+ function presetForEngine(engine, presets = AI_ENGINE_PRESETS) {
661
+ const sameKind = presets.filter((p) => p.kind === engine.kind);
662
+ const exact = sameKind.filter((p) => urlKey(p.baseUrl) === urlKey(engine.baseUrl));
663
+ if (exact.length > 0) {
664
+ return exact.find((p) => p.apiKeyEnv && p.apiKeyEnv === engine.apiKeyEnv) ?? exact.find((p) => !p.apiKeyEnv && !engine.apiKeyEnv) ?? exact[0];
665
+ }
666
+ let host = "";
667
+ try {
668
+ host = new URL(engine.baseUrl.trim()).hostname.toLowerCase();
669
+ } catch {
670
+ }
671
+ if (host) {
672
+ const sameHost = sameKind.find((p) => p.family === "cloud" && new URL(p.baseUrl).hostname.toLowerCase() === host);
673
+ if (sameHost) {
674
+ return sameHost;
675
+ }
676
+ }
677
+ return engine.kind !== "openai-compatible" && sameKind.length === 1 ? sameKind[0] : void 0;
678
+ }
679
+ var AI_LOCAL_DETECT_TIMEOUT_MS = 1500;
680
+ function localDetectCandidates(presets = AI_ENGINE_PRESETS) {
681
+ return presets.filter((p) => p.family === "local" && p.id !== "custom").map((p) => ({ presetId: p.id, baseUrl: p.baseUrl }));
682
+ }
683
+
684
+ // src/ai/discovery.ts
685
+ function isObject(v) {
686
+ return typeof v === "object" && v !== null && !Array.isArray(v);
687
+ }
688
+ function dataArray(body, key = "data") {
689
+ if (!isObject(body) || !Array.isArray(body[key])) {
690
+ return [];
691
+ }
692
+ return body[key].filter(isObject);
693
+ }
694
+ function drafterArch(v) {
695
+ return typeof v === "string" && /_mtp$/i.test(v);
696
+ }
697
+ function moeArch(v) {
698
+ return typeof v === "string" && /moe/i.test(v);
699
+ }
700
+ function positiveInt(v) {
701
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : void 0;
702
+ }
703
+ function parseOpenAiModelList(body) {
704
+ return dataArray(body).filter((m) => typeof m.id === "string" && m.id).map((m) => ({ id: m.id, capabilities: {}, source: "heuristic" }));
705
+ }
706
+ function parseLmStudioModels(body) {
707
+ const out = [];
708
+ for (const m of dataArray(body)) {
709
+ if (typeof m.id !== "string" || !m.id) {
710
+ continue;
711
+ }
712
+ const caps = {};
713
+ if (m.type === "embeddings") {
714
+ Object.assign(caps, { chat: false, stream: false, vision: false, tools: false, json: false, embed: true });
715
+ } else if (m.type === "vlm") {
716
+ Object.assign(caps, { chat: true, vision: true, embed: false });
717
+ } else if (m.type === "llm") {
718
+ Object.assign(caps, { chat: true, vision: false, embed: false });
719
+ }
720
+ if (Array.isArray(m.capabilities) && m.capabilities.includes("tool_use")) {
721
+ caps.tools = true;
722
+ }
723
+ if (drafterArch(m.arch)) {
724
+ Object.assign(caps, { chat: false, stream: false, vision: false, tools: false, json: false, embed: false });
725
+ }
726
+ const ctx = positiveInt(m.max_context_length) ?? positiveInt(m.loaded_context_length);
727
+ if (ctx) {
728
+ caps.contextTokens = ctx;
729
+ }
730
+ const model = { id: m.id, capabilities: caps, source: "endpoint" };
731
+ if (typeof m.state === "string") {
732
+ model.loaded = m.state === "loaded";
733
+ }
734
+ if (moeArch(m.arch)) {
735
+ model.moe = true;
736
+ }
737
+ out.push(model);
738
+ }
739
+ return out;
740
+ }
741
+ function parseOmlxModelStatus(body) {
742
+ const out = [];
743
+ for (const m of dataArray(body, "models")) {
744
+ if (typeof m.id !== "string" || !m.id) {
745
+ continue;
746
+ }
747
+ const caps = {};
748
+ if (m.model_type === "llm") {
749
+ Object.assign(caps, { chat: true, vision: false, embed: false });
750
+ } else if (m.model_type === "vlm") {
751
+ Object.assign(caps, { chat: true, vision: true, embed: false });
752
+ } else if (m.model_type === "embedding" || m.model_type === "embeddings") {
753
+ Object.assign(caps, { chat: false, stream: false, vision: false, tools: false, json: false, embed: true });
754
+ } else if (typeof m.model_type === "string") {
755
+ Object.assign(caps, { chat: false, stream: false, vision: false, tools: false, json: false, embed: false });
756
+ }
757
+ if (drafterArch(m.config_model_type)) {
758
+ Object.assign(caps, { chat: false, stream: false, vision: false, tools: false, json: false, embed: false });
759
+ }
760
+ const ctx = positiveInt(m.model_context_length);
761
+ if (ctx) {
762
+ caps.contextTokens = ctx;
763
+ }
764
+ const model = { id: m.id, capabilities: caps, source: Object.keys(caps).length > 0 ? "endpoint" : "heuristic" };
765
+ if (typeof m.loaded === "boolean") {
766
+ model.loaded = m.loaded;
767
+ }
768
+ const size = positiveInt(m.estimated_size);
769
+ if (size) {
770
+ model.sizeBytes = size;
771
+ }
772
+ if (moeArch(m.config_model_type)) {
773
+ model.moe = true;
774
+ }
775
+ out.push(model);
776
+ }
777
+ return out;
778
+ }
779
+ function withModelStatus(listed, status) {
780
+ const byId = new Map(status.map((s) => [s.id, s]));
781
+ return listed.map((m) => {
782
+ const s = byId.get(m.id);
783
+ if (!s) {
784
+ return m;
785
+ }
786
+ const next = { ...m, capabilities: { ...m.capabilities, ...s.capabilities } };
787
+ if (s.source === "endpoint") {
788
+ next.source = "endpoint";
789
+ }
790
+ if (s.loaded !== void 0) {
791
+ next.loaded = s.loaded;
792
+ }
793
+ if (s.sizeBytes !== void 0) {
794
+ next.sizeBytes = s.sizeBytes;
795
+ }
796
+ if (s.moe) {
797
+ next.moe = true;
798
+ }
799
+ return next;
800
+ });
801
+ }
802
+ function parseModelSizes(body) {
803
+ const out = /* @__PURE__ */ new Map();
804
+ for (const m of dataArray(body, "models")) {
805
+ const id = [m.key, m.model, m.name].find((v) => typeof v === "string" && v.length > 0);
806
+ const size = positiveInt(m.size_bytes) ?? positiveInt(m.size);
807
+ if (id && size) {
808
+ out.set(id, size);
809
+ }
810
+ }
811
+ return out;
812
+ }
813
+ function withModelSizes(models, sizes) {
814
+ return models.map((m) => {
815
+ const size = sizes.get(m.id);
816
+ return size ? { ...m, sizeBytes: size } : m;
817
+ });
818
+ }
819
+ function parseOllamaTags(body) {
820
+ return dataArray(body, "models").map((m) => typeof m.model === "string" && m.model ? m.model : m.name).filter((n) => typeof n === "string" && n.length > 0);
821
+ }
822
+ function parseOllamaShow(name, body) {
823
+ const caps = {};
824
+ if (!isObject(body)) {
825
+ return { id: name, capabilities: caps, source: "heuristic" };
826
+ }
827
+ if (Array.isArray(body.capabilities)) {
828
+ const list = body.capabilities.filter((c) => typeof c === "string");
829
+ caps.chat = list.includes("completion");
830
+ caps.stream = caps.chat;
831
+ caps.vision = list.includes("vision");
832
+ caps.tools = list.includes("tools");
833
+ caps.embed = list.includes("embedding");
834
+ caps.fim = list.includes("insert");
835
+ }
836
+ if (isObject(body.model_info)) {
837
+ const info = body.model_info;
838
+ const arch = typeof info["general.architecture"] === "string" ? info["general.architecture"] : null;
839
+ const ctx = (arch ? positiveInt(info[`${arch}.context_length`]) : void 0) ?? positiveInt(Object.entries(info).find(([k]) => k.endsWith(".context_length"))?.[1]);
840
+ if (ctx) {
841
+ caps.contextTokens = ctx;
842
+ }
843
+ }
844
+ return { id: name, capabilities: caps, source: Object.keys(caps).length > 0 ? "endpoint" : "heuristic" };
845
+ }
846
+ function parseOllamaRunning(body) {
847
+ return new Set(parseOllamaTags(body));
848
+ }
849
+ function parseAnthropicModels(body) {
850
+ const out = [];
851
+ for (const m of dataArray(body)) {
852
+ if (typeof m.id !== "string" || !m.id) {
853
+ continue;
854
+ }
855
+ const caps = { chat: true, stream: true, tools: true, fim: false, embed: false };
856
+ const ctx = positiveInt(m.max_input_tokens);
857
+ if (ctx) {
858
+ caps.contextTokens = ctx;
859
+ }
860
+ const tree = isObject(m.capabilities) ? m.capabilities : null;
861
+ const supported = (key) => {
862
+ const node = tree?.[key];
863
+ return isObject(node) && typeof node.supported === "boolean" ? node.supported : void 0;
864
+ };
865
+ const vision = supported("image_input");
866
+ if (vision !== void 0) {
867
+ caps.vision = vision;
868
+ }
869
+ const json = supported("structured_outputs");
870
+ if (json !== void 0) {
871
+ caps.json = json;
872
+ }
873
+ const model = { id: m.id, capabilities: caps, source: "endpoint" };
874
+ if (typeof m.display_name === "string" && m.display_name) {
875
+ model.label = m.display_name;
876
+ }
877
+ out.push(model);
878
+ }
879
+ return out;
880
+ }
881
+ function anthropicNextPage(body) {
882
+ return isObject(body) && body.has_more === true && typeof body.last_id === "string" ? body.last_id : null;
883
+ }
884
+ function localDetectUrl(baseUrl, detectPath) {
885
+ const base = baseUrl.trim().replace(/\/+$/, "");
886
+ return detectPath ? `${base.replace(/\/v1$/, "")}${detectPath}` : `${base}/models`;
887
+ }
888
+ function countDetectedModels(body) {
889
+ if (!isObject(body)) {
890
+ return void 0;
891
+ }
892
+ if (Array.isArray(body.data)) {
893
+ return dataArray(body).filter((m) => m.type !== "embeddings").length;
894
+ }
895
+ if (Array.isArray(body.models)) {
896
+ return dataArray(body, "models").length;
897
+ }
898
+ return void 0;
899
+ }
900
+ function localDetectTarget(candidate, presets = AI_ENGINE_PRESETS) {
901
+ const preset = presets.find((p) => p.id === candidate.presetId);
902
+ if (!preset || preset.family !== "local" || preset.id === "custom") {
903
+ return null;
904
+ }
905
+ const baseUrl = typeof candidate.baseUrl === "string" ? candidate.baseUrl.trim() : "";
906
+ if (!/^https?:\/\/[^/]/i.test(baseUrl)) {
907
+ return null;
908
+ }
909
+ const target = { presetId: preset.id, baseUrl, url: localDetectUrl(baseUrl, preset.detectPath), kind: preset.kind };
910
+ const own = baseUrl.replace(/\/+$/, "").toLowerCase() === preset.baseUrl.replace(/\/+$/, "").toLowerCase();
911
+ if (own && preset.apiKeyEnv) {
912
+ target.apiKeyEnv = preset.apiKeyEnv;
913
+ }
914
+ if (own && preset.apiKeyFile) {
915
+ target.apiKeyFile = { ...preset.apiKeyFile };
916
+ }
917
+ return target;
918
+ }
919
+
920
+ // src/ai/schema.ts
921
+ var AI_SETTINGS_KEYS = ["enabled", "allowCloud", "engines", "routing", "features", "localNetworks", "showToolbarButton"];
922
+ function isKnownEngineKind(kind) {
923
+ return AI_ENGINE_KINDS.includes(kind);
924
+ }
925
+ function isHostEngineKind(kind) {
926
+ return AI_HOST_ENGINE_KINDS.includes(kind);
927
+ }
928
+ function isObject2(v) {
929
+ return typeof v === "object" && v !== null && !Array.isArray(v);
930
+ }
931
+ function extras(raw, known) {
932
+ const out = {};
933
+ for (const [k, v] of Object.entries(raw)) {
934
+ if (!known.includes(k)) {
935
+ out[k] = v;
936
+ }
937
+ }
938
+ return out;
939
+ }
940
+ function randomSuffix(length) {
941
+ const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
942
+ const bytes = new Uint8Array(length);
943
+ const c = globalThis.crypto;
944
+ if (c?.getRandomValues) {
945
+ c.getRandomValues(bytes);
946
+ } else {
947
+ for (let i = 0; i < length; i++) {
948
+ bytes[i] = Math.floor(Math.random() * 256);
949
+ }
950
+ }
951
+ return Array.from(bytes, (b) => alphabet[b % alphabet.length]).join("");
952
+ }
953
+ function generateAiId(name = "") {
954
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24).replace(/-+$/, "");
955
+ return slug ? `${slug}-${randomSuffix(6)}` : `engine-${randomSuffix(8)}`;
956
+ }
957
+ function parseAiSettings(raw, opts = {}) {
958
+ const warn = opts.onWarning ?? ((m) => console.warn(`[mikedown ai] ${m}`));
959
+ const genId = opts.generateId ?? generateAiId;
960
+ const defaults = defaultAiSettings();
961
+ if (!isObject2(raw)) {
962
+ if (raw !== void 0 && raw !== null) {
963
+ warn("AI settings are not an object; using defaults.");
964
+ }
965
+ return defaults;
966
+ }
967
+ const source = unflatten(raw);
968
+ const out = { ...extras(source, AI_SETTINGS_KEYS), ...defaults };
969
+ if (typeof source.enabled === "boolean") {
970
+ out.enabled = source.enabled;
971
+ }
972
+ if (typeof source.allowCloud === "boolean") {
973
+ out.allowCloud = source.allowCloud;
974
+ }
975
+ if (typeof source.showToolbarButton === "boolean") {
976
+ out.showToolbarButton = source.showToolbarButton;
977
+ }
978
+ if (Array.isArray(source.localNetworks)) {
979
+ out.localNetworks = source.localNetworks.filter((n) => typeof n === "string" && n.trim().length > 0);
980
+ } else if (source.localNetworks !== void 0) {
981
+ warn("localNetworks is not an array; using the default.");
982
+ }
983
+ out.engines = parseEngines(source.engines, genId, warn);
984
+ out.routing = parseRouting(source.routing, warn);
985
+ out.features = parseFeatures(source.features, warn);
986
+ return out;
987
+ }
988
+ function unflatten(raw) {
989
+ const prefixed = Object.keys(raw).filter((k) => k.startsWith("ai."));
990
+ if (prefixed.length === 0) {
991
+ return isObject2(raw.ai) ? raw.ai : raw;
992
+ }
993
+ const out = {};
994
+ for (const k of prefixed) {
995
+ out[k.slice(3)] = raw[k];
996
+ }
997
+ return out;
998
+ }
999
+ var ENGINE_KEYS = ["id", "name", "kind", "baseUrl", "enabled", "locality", "apiKeyEnv", "apiKeyFile", "discovery", "models"];
1000
+ function parseApiKeyFile(raw) {
1001
+ if (!isObject2(raw) || typeof raw.path !== "string" || typeof raw.jsonPath !== "string") {
1002
+ return void 0;
1003
+ }
1004
+ const path = raw.path.trim();
1005
+ const jsonPath = raw.jsonPath.trim();
1006
+ return path && jsonPath ? { ...extras(raw, ["path", "jsonPath"]), path, jsonPath } : void 0;
1007
+ }
1008
+ function parseEngines(raw, genId, warn) {
1009
+ if (raw === void 0) {
1010
+ return [];
1011
+ }
1012
+ if (!Array.isArray(raw)) {
1013
+ warn("engines is not an array; ignoring it.");
1014
+ return [];
1015
+ }
1016
+ const engines = [];
1017
+ const ids = /* @__PURE__ */ new Set();
1018
+ raw.forEach((entry, index) => {
1019
+ if (!isObject2(entry) || typeof entry.baseUrl !== "string") {
1020
+ warn(`engines[${index}] has no baseUrl; dropped.`);
1021
+ return;
1022
+ }
1023
+ const name = typeof entry.name === "string" && entry.name.trim() ? entry.name : entry.baseUrl;
1024
+ let kind = typeof entry.kind === "string" ? entry.kind : "openai-compatible";
1025
+ if (!kind.trim()) {
1026
+ kind = "openai-compatible";
1027
+ }
1028
+ let id = typeof entry.id === "string" && entry.id.trim() ? entry.id : "";
1029
+ if (!id || ids.has(id)) {
1030
+ if (id) {
1031
+ warn(`engines[${index}] repeats id "${id}"; gave it a new one.`);
1032
+ }
1033
+ do {
1034
+ id = genId(name);
1035
+ } while (ids.has(id));
1036
+ }
1037
+ ids.add(id);
1038
+ const engine = { ...extras(entry, ENGINE_KEYS) };
1039
+ engine.id = id;
1040
+ engine.name = name;
1041
+ engine.kind = kind;
1042
+ engine.baseUrl = entry.baseUrl.trim();
1043
+ engine.enabled = typeof entry.enabled === "boolean" ? entry.enabled : true;
1044
+ engine.locality = entry.locality === "local" || entry.locality === "cloud" ? entry.locality : "auto";
1045
+ if (typeof entry.apiKeyEnv === "string" && entry.apiKeyEnv.trim()) {
1046
+ engine.apiKeyEnv = entry.apiKeyEnv.trim();
1047
+ }
1048
+ if (entry.apiKeyFile !== void 0) {
1049
+ const file = parseApiKeyFile(entry.apiKeyFile);
1050
+ if (file) {
1051
+ engine.apiKeyFile = file;
1052
+ } else {
1053
+ warn(`engines[${index}].apiKeyFile needs a path and a jsonPath (both strings); dropped.`);
1054
+ }
1055
+ }
1056
+ engine.discovery = entry.discovery === "manual" ? "manual" : "auto";
1057
+ engine.models = parseModels(entry.models, engine.kind, `engines[${index}]`, warn);
1058
+ engines.push(engine);
1059
+ });
1060
+ return engines;
1061
+ }
1062
+ var MODEL_KEYS = ["id", "label", "enabled", "capabilities", "capabilitiesSource", "requestOverrides", "missing", "manual"];
1063
+ var CAPABILITY_SOURCES = ["endpoint", "heuristic", "user"];
1064
+ function parseModels(raw, kind, where, warn) {
1065
+ if (raw === void 0) {
1066
+ return [];
1067
+ }
1068
+ if (!Array.isArray(raw)) {
1069
+ warn(`${where}.models is not an array; ignoring it.`);
1070
+ return [];
1071
+ }
1072
+ const models = [];
1073
+ const ids = /* @__PURE__ */ new Set();
1074
+ raw.forEach((entry, index) => {
1075
+ if (!isObject2(entry) || typeof entry.id !== "string" || !entry.id.trim()) {
1076
+ warn(`${where}.models[${index}] has no id; dropped.`);
1077
+ return;
1078
+ }
1079
+ if (ids.has(entry.id)) {
1080
+ warn(`${where}.models[${index}] repeats id "${entry.id}"; dropped.`);
1081
+ return;
1082
+ }
1083
+ ids.add(entry.id);
1084
+ const model = { ...extras(entry, MODEL_KEYS) };
1085
+ model.id = entry.id;
1086
+ if (typeof entry.label === "string" && entry.label.trim()) {
1087
+ model.label = entry.label;
1088
+ }
1089
+ model.enabled = typeof entry.enabled === "boolean" ? entry.enabled : true;
1090
+ const known = isKnownEngineKind(kind) ? kind : void 0;
1091
+ if (isObject2(entry.capabilities)) {
1092
+ model.capabilities = completeCapabilities(entry.capabilities, entry.id, known);
1093
+ if (entry.capabilities.contextTokens === null) {
1094
+ model.capabilities.contextTokens = null;
1095
+ }
1096
+ model.capabilitiesSource = CAPABILITY_SOURCES.includes(entry.capabilitiesSource) ? entry.capabilitiesSource : "heuristic";
1097
+ } else {
1098
+ model.capabilities = detectCapabilities(entry.id, known).capabilities;
1099
+ model.capabilitiesSource = "heuristic";
1100
+ }
1101
+ if (isObject2(entry.requestOverrides)) {
1102
+ model.requestOverrides = entry.requestOverrides;
1103
+ } else if (entry.requestOverrides !== void 0) {
1104
+ warn(`${where}.models[${index}].requestOverrides is not an object; ignoring it.`);
1105
+ }
1106
+ if (entry.missing === true) {
1107
+ model.missing = true;
1108
+ }
1109
+ if (entry.manual === true) {
1110
+ model.manual = true;
1111
+ }
1112
+ models.push(model);
1113
+ });
1114
+ return models;
1115
+ }
1116
+ function parseRoute(raw) {
1117
+ if (!isObject2(raw) || typeof raw.engineId !== "string" || typeof raw.modelId !== "string" || !raw.engineId || !raw.modelId) {
1118
+ return null;
1119
+ }
1120
+ return { ...raw, engineId: raw.engineId, modelId: raw.modelId };
1121
+ }
1122
+ function parseRouting(raw, warn) {
1123
+ const routing = { default: null, fallback: "anyLocal", tasks: {} };
1124
+ if (raw === void 0) {
1125
+ return routing;
1126
+ }
1127
+ if (!isObject2(raw)) {
1128
+ warn("routing is not an object; using the defaults.");
1129
+ return routing;
1130
+ }
1131
+ const out = { ...extras(raw, ["default", "fallback", "tasks"]), ...routing };
1132
+ if (raw.default !== void 0 && raw.default !== null) {
1133
+ out.default = parseRoute(raw.default);
1134
+ if (!out.default) {
1135
+ warn("routing.default needs engineId and modelId; cleared.");
1136
+ }
1137
+ }
1138
+ if (typeof raw.fallback === "string" && AI_FALLBACK_MODES.includes(raw.fallback)) {
1139
+ out.fallback = raw.fallback;
1140
+ } else if (raw.fallback !== void 0) {
1141
+ warn(`routing.fallback "${String(raw.fallback)}" is not a fallback mode; using anyLocal.`);
1142
+ }
1143
+ if (isObject2(raw.tasks)) {
1144
+ for (const [task, value] of Object.entries(raw.tasks)) {
1145
+ const route = parseRoute(value);
1146
+ if (route) {
1147
+ out.tasks[task] = route;
1148
+ } else if (value !== null) {
1149
+ warn(`routing.tasks.${task} needs engineId and modelId; dropped.`);
1150
+ }
1151
+ }
1152
+ } else if (raw.tasks !== void 0) {
1153
+ warn("routing.tasks is not an object; ignoring it.");
1154
+ }
1155
+ return out;
1156
+ }
1157
+ function parseFeatures(raw, warn) {
1158
+ const features = {};
1159
+ if (raw === void 0) {
1160
+ return features;
1161
+ }
1162
+ if (!isObject2(raw)) {
1163
+ warn("features is not an object; ignoring it.");
1164
+ return features;
1165
+ }
1166
+ for (const [id, value] of Object.entries(raw)) {
1167
+ if (!isObject2(value)) {
1168
+ warn(`features.${id} is not an object; dropped.`);
1169
+ continue;
1170
+ }
1171
+ const feature = { ...extras(value, ["enabled", "task"]) };
1172
+ if (typeof value.enabled === "boolean") {
1173
+ feature.enabled = value.enabled;
1174
+ }
1175
+ if (typeof value.task === "string" && value.task.trim()) {
1176
+ feature.task = value.task;
1177
+ }
1178
+ features[id] = feature;
1179
+ }
1180
+ return features;
1181
+ }
1182
+ function serializeAiSettings(settings) {
1183
+ const patch = {};
1184
+ for (const [key, value] of Object.entries(settings)) {
1185
+ if (value === void 0) {
1186
+ continue;
1187
+ }
1188
+ patch[`ai.${key}`] = JSON.parse(JSON.stringify(value));
1189
+ }
1190
+ return patch;
1191
+ }
1192
+
1193
+ // src/ai/router.ts
1194
+ function isRouteUsable(r) {
1195
+ return !("unusable" in r);
1196
+ }
1197
+ var CAPABILITY_LABELS = {
1198
+ chat: "chat",
1199
+ stream: "streaming",
1200
+ vision: "vision",
1201
+ json: "JSON output",
1202
+ tools: "tools",
1203
+ fim: "fill-in-the-middle",
1204
+ embed: "embeddings",
1205
+ contextTokens: "a large enough context window"
1206
+ };
1207
+ function engineLabel(engine) {
1208
+ return engine.name || engine.id;
1209
+ }
1210
+ function modelLabel(model) {
1211
+ return model.label || model.id;
1212
+ }
1213
+ function resolveRoute(settings, statuses, consent, opts) {
1214
+ if (!settings.enabled) {
1215
+ return { unusable: "aiDisabled", reason: "AI is turned off." };
1216
+ }
1217
+ const feature = resolveAiFeature(settings.features, opts.feature);
1218
+ const task = opts.task ?? feature.task;
1219
+ if (!feature.enabled) {
1220
+ return { unusable: "featureDisabled", reason: `${feature.definition?.label ?? opts.feature} is turned off.`, task };
1221
+ }
1222
+ const taskDef = getAiTask(task);
1223
+ const requires = combineRequirements(taskDef?.requires ?? {}, feature.requires);
1224
+ const requiresAny = taskDef?.requiresAny;
1225
+ const prefers = taskDef?.prefers;
1226
+ const statusById = new Map(statuses.map((s) => [s.engineId, s]));
1227
+ const check = (route) => checkCandidate(settings, statusById, consent, route, requires, requiresAny);
1228
+ const preferred = settings.routing.tasks[task] ?? settings.routing.default;
1229
+ if (!preferred) {
1230
+ return fallbackFrom(null, { code: "noRoute", reason: "No engine and model is chosen for this task." });
1231
+ }
1232
+ const first = check(preferred);
1233
+ if (!("code" in first)) {
1234
+ return ok(first, false);
1235
+ }
1236
+ return fallbackFrom(preferred, first);
1237
+ function ok(c, viaFallback, reason) {
1238
+ const result = { engineId: c.engine.id, modelId: c.model.id, viaFallback, task, isLocal: c.isLocal, needsConsent: c.needsConsent };
1239
+ if (reason) {
1240
+ result.reason = reason;
1241
+ }
1242
+ return result;
1243
+ }
1244
+ function fallbackFrom(primary, rejection) {
1245
+ const mode = settings.routing.fallback;
1246
+ if (mode === "none") {
1247
+ return unusableFrom(rejection);
1248
+ }
1249
+ const primaryEngine = primary ? settings.engines.find((e) => e.id === primary.engineId) : void 0;
1250
+ const primaryIsLocal = primaryEngine ? engineLocality(primaryEngine, settings.localNetworks) === "local" : true;
1251
+ const allowsCloud = (c) => c.isLocal || !primaryIsLocal || mode === "any";
1252
+ const tried = /* @__PURE__ */ new Set();
1253
+ if (primary) {
1254
+ tried.add(routeKey(primary));
1255
+ }
1256
+ const tier = [];
1257
+ const def = settings.routing.default;
1258
+ if (def && !tried.has(routeKey(def))) {
1259
+ tried.add(routeKey(def));
1260
+ const c = check(def);
1261
+ if (!("code" in c) && allowsCloud(c)) {
1262
+ tier.push(c);
1263
+ }
1264
+ }
1265
+ if (mode === "default") {
1266
+ const only = preferFirst(tier, prefers);
1267
+ return only ? ok(only, true, rejection.reason) : unusableFrom(rejection);
1268
+ }
1269
+ const scan = (wantLocal) => {
1270
+ const found = [];
1271
+ for (const engine of settings.engines) {
1272
+ for (const model of engine.models) {
1273
+ const route = { engineId: engine.id, modelId: model.id };
1274
+ if (tried.has(routeKey(route))) {
1275
+ continue;
1276
+ }
1277
+ const c = check(route);
1278
+ if ("code" in c || c.isLocal !== wantLocal) {
1279
+ continue;
1280
+ }
1281
+ found.push(c);
1282
+ }
1283
+ }
1284
+ return found;
1285
+ };
1286
+ const pick = preferFirst([...tier, ...scan(true)], prefers);
1287
+ if (pick) {
1288
+ return ok(pick, true, rejection.reason);
1289
+ }
1290
+ if (mode === "any") {
1291
+ const cloud = preferFirst(scan(false), prefers);
1292
+ if (cloud) {
1293
+ return ok(cloud, true, rejection.reason);
1294
+ }
1295
+ }
1296
+ return unusableFrom(rejection);
1297
+ }
1298
+ function unusableFrom(rejection) {
1299
+ return { unusable: rejection.code, reason: rejection.reason, task, ...rejection.engineId ? { engineId: rejection.engineId } : {} };
1300
+ }
1301
+ }
1302
+ function preferFirst(candidates, prefers) {
1303
+ if (candidates.length === 0) {
1304
+ return null;
1305
+ }
1306
+ if (prefers) {
1307
+ const match = candidates.find((c) => missingCapabilities(c.model.capabilities, prefers).length === 0);
1308
+ if (match) {
1309
+ return match;
1310
+ }
1311
+ }
1312
+ return candidates[0];
1313
+ }
1314
+ function routeKey(r) {
1315
+ return `${r.engineId}\0${r.modelId}`;
1316
+ }
1317
+ function checkCandidate(settings, statusById, consent, route, requires, requiresAny) {
1318
+ const engine = settings.engines.find((e) => e.id === route.engineId);
1319
+ if (!engine) {
1320
+ return { code: "engineMissing", reason: "The routed engine no longer exists." };
1321
+ }
1322
+ const name = engineLabel(engine);
1323
+ const engineId = engine.id;
1324
+ if (!engine.enabled) {
1325
+ return { code: "engineDisabled", reason: `${name} is turned off.`, engineId };
1326
+ }
1327
+ if (!isKnownEngineKind(engine.kind)) {
1328
+ return { code: "engineUnsupported", reason: `${name} uses an engine kind this version does not support.` };
1329
+ }
1330
+ const status = statusById.get(engine.id);
1331
+ if (isHostEngineKind(engine.kind) && !status) {
1332
+ return { code: "engineUnsupported", reason: `${name} uses an engine kind this app does not support.` };
1333
+ }
1334
+ const model = engine.models.find((m) => m.id === route.modelId);
1335
+ if (!model) {
1336
+ return { code: "modelMissing", reason: `${name} has no model "${route.modelId}".`, engineId };
1337
+ }
1338
+ if (!model.enabled) {
1339
+ return { code: "modelDisabled", reason: `${modelLabel(model)} is turned off.` };
1340
+ }
1341
+ if (model.missing || status?.models.find((m) => m.id === model.id)?.missing) {
1342
+ return { code: "modelNotListed", reason: `${name} no longer lists ${modelLabel(model)}.`, engineId };
1343
+ }
1344
+ const lacking = missingCapabilities(model.capabilities, requires, requiresAny);
1345
+ if (lacking.length > 0) {
1346
+ return { code: "capability", reason: `${modelLabel(model)} lacks ${lacking.map((c) => CAPABILITY_LABELS[c] ?? c).join(", ")}.` };
1347
+ }
1348
+ const isLocal = engineLocality(engine, settings.localNetworks) === "local";
1349
+ let needsConsent = false;
1350
+ if (!isLocal) {
1351
+ if (!settings.allowCloud) {
1352
+ return { code: "cloudBlocked", reason: `${name} is a cloud engine and Allow cloud is off.` };
1353
+ }
1354
+ const answer = consent[engine.id];
1355
+ if (answer === false) {
1356
+ return { code: "consentDenied", reason: `${name} was not allowed in this workspace.` };
1357
+ }
1358
+ needsConsent = answer !== true;
1359
+ }
1360
+ if (status?.ok === false) {
1361
+ return { code: "probeFailed", reason: status.error ? `${name} failed its last connection test: ${status.error}` : `${name} failed its last connection test.`, engineId };
1362
+ }
1363
+ return { engine, model, isLocal, needsConsent };
1364
+ }
1365
+
1366
+ // src/ai/sse.ts
1367
+ var SseParser = class {
1368
+ constructor() {
1369
+ this.buffer = "";
1370
+ this.started = false;
1371
+ this.dataLines = [];
1372
+ }
1373
+ /** Feed decoded text; returns the events it completed. */
1374
+ push(chunk) {
1375
+ let text = chunk;
1376
+ if (!this.started && text.length > 0) {
1377
+ this.started = true;
1378
+ if (text.charCodeAt(0) === 65279) {
1379
+ text = text.slice(1);
1380
+ }
1381
+ }
1382
+ this.buffer += text;
1383
+ const events = [];
1384
+ for (; ; ) {
1385
+ const match = /\r\n|\r|\n/.exec(this.buffer);
1386
+ if (!match) {
1387
+ break;
1388
+ }
1389
+ if (match[0] === "\r" && match.index === this.buffer.length - 1) {
1390
+ break;
1391
+ }
1392
+ const line = this.buffer.slice(0, match.index);
1393
+ this.buffer = this.buffer.slice(match.index + match[0].length);
1394
+ const event = this.line(line);
1395
+ if (event) {
1396
+ events.push(event);
1397
+ }
1398
+ }
1399
+ return events;
1400
+ }
1401
+ /** The stream closed: flush a final event that had no trailing blank line. */
1402
+ end() {
1403
+ const events = [];
1404
+ if (this.buffer.length > 0) {
1405
+ const rest = this.buffer.replace(/\r$/, "");
1406
+ this.buffer = "";
1407
+ const event = this.line(rest);
1408
+ if (event) {
1409
+ events.push(event);
1410
+ }
1411
+ }
1412
+ const last = this.line("");
1413
+ if (last) {
1414
+ events.push(last);
1415
+ }
1416
+ return events;
1417
+ }
1418
+ line(line) {
1419
+ if (line === "") {
1420
+ if (this.dataLines.length === 0) {
1421
+ this.eventName = void 0;
1422
+ return null;
1423
+ }
1424
+ const event = { data: this.dataLines.join("\n") };
1425
+ if (this.eventName) {
1426
+ event.event = this.eventName;
1427
+ }
1428
+ if (this.lastId !== void 0) {
1429
+ event.id = this.lastId;
1430
+ }
1431
+ this.dataLines = [];
1432
+ this.eventName = void 0;
1433
+ return event;
1434
+ }
1435
+ if (line.startsWith(":")) {
1436
+ return null;
1437
+ }
1438
+ const colon = line.indexOf(":");
1439
+ const field = colon === -1 ? line : line.slice(0, colon);
1440
+ let value = colon === -1 ? "" : line.slice(colon + 1);
1441
+ if (value.startsWith(" ")) {
1442
+ value = value.slice(1);
1443
+ }
1444
+ if (field === "data") {
1445
+ this.dataLines.push(value);
1446
+ } else if (field === "event") {
1447
+ this.eventName = value;
1448
+ } else if (field === "id" && !value.includes("\0")) {
1449
+ this.lastId = value;
1450
+ }
1451
+ return null;
1452
+ }
1453
+ };
1454
+ function parseJson(data) {
1455
+ try {
1456
+ const v = JSON.parse(data);
1457
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
1458
+ } catch {
1459
+ return null;
1460
+ }
1461
+ }
1462
+ function num(v) {
1463
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
1464
+ }
1465
+ function errorCodeFromType(type) {
1466
+ switch (type) {
1467
+ case "authentication_error":
1468
+ case "permission_error":
1469
+ case "invalid_api_key":
1470
+ return "auth";
1471
+ case "not_found_error":
1472
+ case "model_not_found":
1473
+ return "modelNotFound";
1474
+ case "rate_limit_error":
1475
+ case "overloaded_error":
1476
+ case "rate_limit_exceeded":
1477
+ return "rateLimited";
1478
+ case "context_length_exceeded":
1479
+ case "request_too_large":
1480
+ return "contextOverflow";
1481
+ default:
1482
+ return "unknown";
1483
+ }
1484
+ }
1485
+ function streamError(raw) {
1486
+ const e = typeof raw === "object" && raw !== null ? raw : {};
1487
+ const message = typeof e.message === "string" ? e.message : typeof raw === "string" ? raw : "The stream reported an error.";
1488
+ return { code: errorCodeFromType(e.type ?? e.code), message };
1489
+ }
1490
+ function mapOpenAiFinish(reason) {
1491
+ if (reason === "length") {
1492
+ return "length";
1493
+ }
1494
+ if (reason === "stop" || reason === "tool_calls" || reason === "function_call" || reason === "eos") {
1495
+ return "stop";
1496
+ }
1497
+ return "error";
1498
+ }
1499
+ function mapAnthropicStop(reason) {
1500
+ if (reason === "max_tokens" || reason === "model_context_window_exceeded") {
1501
+ return "length";
1502
+ }
1503
+ if (reason === "end_turn" || reason === "stop_sequence" || reason === "tool_use" || reason === "pause_turn") {
1504
+ return "stop";
1505
+ }
1506
+ return "error";
1507
+ }
1508
+ function createOpenAiStreamReader() {
1509
+ const parser = new SseParser();
1510
+ let text = "";
1511
+ let reasoningChars = 0;
1512
+ let done = false;
1513
+ let stopReason;
1514
+ let usage;
1515
+ let error;
1516
+ const handle = (events) => {
1517
+ const deltas = [];
1518
+ for (const ev of events) {
1519
+ const data = ev.data.trim();
1520
+ if (data === "[DONE]") {
1521
+ done = true;
1522
+ continue;
1523
+ }
1524
+ const json = parseJson(data);
1525
+ if (!json) {
1526
+ continue;
1527
+ }
1528
+ if (json.error !== void 0) {
1529
+ error = streamError(json.error);
1530
+ continue;
1531
+ }
1532
+ const u = json.usage;
1533
+ if (u && typeof u === "object") {
1534
+ usage = { promptTokens: num(u.prompt_tokens), completionTokens: num(u.completion_tokens) };
1535
+ }
1536
+ const choices = Array.isArray(json.choices) ? json.choices : [];
1537
+ for (const choice of choices) {
1538
+ const delta = choice.delta;
1539
+ const content = delta?.content;
1540
+ if (typeof content === "string" && content) {
1541
+ text += content;
1542
+ deltas.push(content);
1543
+ }
1544
+ const reasoning = delta?.reasoning_content ?? delta?.reasoning;
1545
+ if (typeof reasoning === "string") {
1546
+ reasoningChars += reasoning.length;
1547
+ }
1548
+ if (typeof choice.finish_reason === "string" && choice.finish_reason) {
1549
+ stopReason = choice.finish_reason;
1550
+ }
1551
+ }
1552
+ }
1553
+ return deltas;
1554
+ };
1555
+ return {
1556
+ push: (chunk) => handle(parser.push(chunk)),
1557
+ outputChars: () => text.length + reasoningChars,
1558
+ finish() {
1559
+ handle(parser.end());
1560
+ let finishReason;
1561
+ if (error) {
1562
+ finishReason = "error";
1563
+ } else if (!done) {
1564
+ finishReason = "error";
1565
+ } else {
1566
+ finishReason = stopReason ? mapOpenAiFinish(stopReason) : "stop";
1567
+ }
1568
+ const outcome = { text, finishReason };
1569
+ if (usage) {
1570
+ outcome.usage = usage;
1571
+ }
1572
+ if (stopReason) {
1573
+ outcome.stopReason = stopReason;
1574
+ }
1575
+ if (error) {
1576
+ outcome.error = error;
1577
+ }
1578
+ return outcome;
1579
+ }
1580
+ };
1581
+ }
1582
+ function createAnthropicStreamReader() {
1583
+ const parser = new SseParser();
1584
+ let text = "";
1585
+ let thinkingChars = 0;
1586
+ let done = false;
1587
+ let stopReason;
1588
+ let promptTokens;
1589
+ let completionTokens;
1590
+ let error;
1591
+ const handle = (events) => {
1592
+ const deltas = [];
1593
+ for (const ev of events) {
1594
+ const json = parseJson(ev.data);
1595
+ if (!json) {
1596
+ continue;
1597
+ }
1598
+ const type = typeof json.type === "string" ? json.type : ev.event;
1599
+ switch (type) {
1600
+ case "message_start": {
1601
+ const message = json.message;
1602
+ const u = message?.usage;
1603
+ promptTokens = num(u?.input_tokens) ?? promptTokens;
1604
+ completionTokens = num(u?.output_tokens) ?? completionTokens;
1605
+ break;
1606
+ }
1607
+ case "content_block_delta": {
1608
+ const delta = json.delta;
1609
+ if (delta?.type === "text_delta" && typeof delta.text === "string" && delta.text) {
1610
+ text += delta.text;
1611
+ deltas.push(delta.text);
1612
+ } else if (delta?.type === "thinking_delta" && typeof delta.thinking === "string") {
1613
+ thinkingChars += delta.thinking.length;
1614
+ }
1615
+ break;
1616
+ }
1617
+ case "message_delta": {
1618
+ const delta = json.delta;
1619
+ if (typeof delta?.stop_reason === "string") {
1620
+ stopReason = delta.stop_reason;
1621
+ }
1622
+ const u = json.usage;
1623
+ promptTokens = num(u?.input_tokens) ?? promptTokens;
1624
+ completionTokens = num(u?.output_tokens) ?? completionTokens;
1625
+ break;
1626
+ }
1627
+ case "message_stop":
1628
+ done = true;
1629
+ break;
1630
+ case "error":
1631
+ error = streamError(json.error);
1632
+ break;
1633
+ default:
1634
+ break;
1635
+ }
1636
+ }
1637
+ return deltas;
1638
+ };
1639
+ return {
1640
+ push: (chunk) => handle(parser.push(chunk)),
1641
+ outputChars: () => text.length + thinkingChars,
1642
+ finish() {
1643
+ handle(parser.end());
1644
+ let finishReason;
1645
+ if (error || !done) {
1646
+ finishReason = "error";
1647
+ } else {
1648
+ finishReason = stopReason ? mapAnthropicStop(stopReason) : "stop";
1649
+ }
1650
+ const outcome = { text, finishReason };
1651
+ if (promptTokens !== void 0 || completionTokens !== void 0) {
1652
+ outcome.usage = { promptTokens, completionTokens };
1653
+ }
1654
+ if (stopReason) {
1655
+ outcome.stopReason = stopReason;
1656
+ }
1657
+ if (error) {
1658
+ outcome.error = error;
1659
+ }
1660
+ return outcome;
1661
+ }
1662
+ };
1663
+ }
1664
+ function createCompletionStreamReader(kind) {
1665
+ return kind === "anthropic" ? createAnthropicStreamReader() : createOpenAiStreamReader();
1666
+ }
1667
+
1668
+ // src/ai/stats.ts
1669
+ var AI_CHARS_PER_TOKEN = 4;
1670
+ function positive(n) {
1671
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
1672
+ }
1673
+ function aiResultStats(result) {
1674
+ const t = result.timing;
1675
+ if (!t || typeof t.totalMs !== "number" || !Number.isFinite(t.totalMs) || t.totalMs < 0) {
1676
+ return null;
1677
+ }
1678
+ const stats = { totalMs: t.totalMs, approximate: false };
1679
+ if (typeof t.firstTokenMs === "number" && Number.isFinite(t.firstTokenMs) && t.firstTokenMs >= 0) {
1680
+ stats.firstTokenMs = t.firstTokenMs;
1681
+ }
1682
+ const reported = positive(result.usage?.completionTokens);
1683
+ if (reported !== void 0) {
1684
+ stats.outputTokens = reported;
1685
+ } else {
1686
+ const chars = t.outputChars ?? result.text.length;
1687
+ if (chars > 0) {
1688
+ stats.outputTokens = Math.max(1, Math.round(chars / AI_CHARS_PER_TOKEN));
1689
+ stats.approximate = true;
1690
+ }
1691
+ }
1692
+ const input = positive(result.usage?.promptTokens);
1693
+ if (input !== void 0) {
1694
+ stats.inputTokens = input;
1695
+ }
1696
+ const first = stats.firstTokenMs;
1697
+ const generationMs = first !== void 0 && t.totalMs > first ? t.totalMs - first : t.totalMs;
1698
+ if (stats.outputTokens !== void 0 && generationMs > 0) {
1699
+ stats.tokensPerSecond = stats.outputTokens / (generationMs / 1e3);
1700
+ }
1701
+ return stats;
1702
+ }
1703
+ function formatAiSeconds(ms) {
1704
+ const s = ms / 1e3;
1705
+ return s < 10 ? `${s.toFixed(1)} s` : `${Math.round(s)} s`;
1706
+ }
1707
+ function formatAiTokensPerSecond(tps, approximate) {
1708
+ const n = tps < 10 ? tps.toFixed(1) : String(Math.round(tps));
1709
+ return `${approximate ? "~" : ""}${n} tok/s`;
1710
+ }
1711
+ function aiResultStatsText(stats) {
1712
+ const time = formatAiSeconds(stats.totalMs);
1713
+ return stats.tokensPerSecond === void 0 ? time : `${time} \xB7 ${formatAiTokensPerSecond(stats.tokensPerSecond, stats.approximate)}`;
1714
+ }
1715
+ function aiResultStatsTitle(stats) {
1716
+ const lines = [];
1717
+ if (stats.firstTokenMs !== void 0) {
1718
+ lines.push(`First token after ${formatAiSeconds(stats.firstTokenMs)}`);
1719
+ }
1720
+ lines.push(`Total ${formatAiSeconds(stats.totalMs)}`);
1721
+ if (stats.outputTokens !== void 0) {
1722
+ lines.push(stats.approximate ? `About ${stats.outputTokens.toLocaleString("en-US")} output tokens (estimated from the text)` : `${stats.outputTokens.toLocaleString("en-US")} output tokens`);
1723
+ }
1724
+ if (stats.inputTokens !== void 0) {
1725
+ lines.push(`${stats.inputTokens.toLocaleString("en-US")} input tokens`);
1726
+ }
1727
+ if (stats.tokensPerSecond !== void 0) {
1728
+ lines.push(formatAiTokensPerSecond(stats.tokensPerSecond, stats.approximate));
1729
+ }
1730
+ return lines.join("\n");
1731
+ }
1732
+ function aiTimingLogParts(result) {
1733
+ const stats = aiResultStats(result);
1734
+ if (!stats) {
1735
+ return [];
1736
+ }
1737
+ const parts = [];
1738
+ if (stats.firstTokenMs !== void 0) {
1739
+ parts.push(`first token ${Math.round(stats.firstTokenMs)} ms`);
1740
+ }
1741
+ if (stats.tokensPerSecond !== void 0) {
1742
+ parts.push(formatAiTokensPerSecond(stats.tokensPerSecond, stats.approximate));
1743
+ }
1744
+ return parts;
1745
+ }
1746
+
1747
+ // src/ai/requests.ts
1748
+ var ANTHROPIC_API_VERSION = "2026-10-21";
1749
+ var ANTHROPIC_DEFAULT_MAX_TOKENS = 4096;
1750
+ var JSON_ONLY_INSTRUCTION = "Respond with a single valid JSON value and nothing else.";
1751
+ function trimSlash(url) {
1752
+ return url.trim().replace(/\/+$/, "");
1753
+ }
1754
+ function engineEndpoints(engine) {
1755
+ const base = trimSlash(engine.baseUrl);
1756
+ const root = base.replace(/\/v1$/, "");
1757
+ const v1 = `${root}/v1`;
1758
+ switch (engine.kind) {
1759
+ case "anthropic":
1760
+ return { completions: `${v1}/messages`, models: `${v1}/models` };
1761
+ case "lmstudio":
1762
+ return { completions: `${v1}/chat/completions`, models: `${v1}/models`, embeddings: `${v1}/embeddings`, nativeModels: `${root}/api/v0/models` };
1763
+ case "ollama":
1764
+ return {
1765
+ completions: `${v1}/chat/completions`,
1766
+ models: `${v1}/models`,
1767
+ embeddings: `${v1}/embeddings`,
1768
+ nativeModels: `${root}/api/tags`,
1769
+ nativeShow: `${root}/api/show`
1770
+ };
1771
+ default:
1772
+ return { completions: `${base}/chat/completions`, models: `${base}/models`, embeddings: `${base}/embeddings` };
1773
+ }
1774
+ }
1775
+ function buildAuthHeaders(kind, apiKey) {
1776
+ const headers = { "content-type": "application/json" };
1777
+ if (kind === "anthropic") {
1778
+ headers["anthropic-version"] = ANTHROPIC_API_VERSION;
1779
+ if (apiKey) {
1780
+ headers["x-api-key"] = apiKey;
1781
+ }
1782
+ } else if (apiKey) {
1783
+ headers.authorization = `Bearer ${apiKey}`;
1784
+ }
1785
+ return headers;
1786
+ }
1787
+ function isPlainObject(v) {
1788
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1789
+ }
1790
+ function mergeRequestOverrides(body, overrides) {
1791
+ if (!overrides) {
1792
+ return body;
1793
+ }
1794
+ const out = { ...body };
1795
+ for (const [key, value] of Object.entries(overrides)) {
1796
+ const current = out[key];
1797
+ out[key] = isPlainObject(current) && isPlainObject(value) ? mergeRequestOverrides(current, value) : value;
1798
+ }
1799
+ return out;
1800
+ }
1801
+ var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1802
+ function bytesToBase64(bytes) {
1803
+ let out = "";
1804
+ let i = 0;
1805
+ for (; i + 2 < bytes.length; i += 3) {
1806
+ const n = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
1807
+ out += BASE64_ALPHABET[n >> 18 & 63] + BASE64_ALPHABET[n >> 12 & 63] + BASE64_ALPHABET[n >> 6 & 63] + BASE64_ALPHABET[n & 63];
1808
+ }
1809
+ const rest = bytes.length - i;
1810
+ if (rest === 1) {
1811
+ const n = bytes[i] << 16;
1812
+ out += `${BASE64_ALPHABET[n >> 18 & 63]}${BASE64_ALPHABET[n >> 12 & 63]}==`;
1813
+ } else if (rest === 2) {
1814
+ const n = bytes[i] << 16 | bytes[i + 1] << 8;
1815
+ out += `${BASE64_ALPHABET[n >> 18 & 63]}${BASE64_ALPHABET[n >> 12 & 63]}${BASE64_ALPHABET[n >> 6 & 63]}=`;
1816
+ }
1817
+ return out;
1818
+ }
1819
+ function base64ToBytes(b64) {
1820
+ const clean = b64.replace(/[\s=]/g, "").replace(/-/g, "+").replace(/_/g, "/");
1821
+ const out = new Uint8Array(Math.floor(clean.length * 3 / 4));
1822
+ let bits = 0;
1823
+ let value = 0;
1824
+ let o = 0;
1825
+ for (const ch of clean) {
1826
+ const idx = BASE64_ALPHABET.indexOf(ch);
1827
+ if (idx === -1) {
1828
+ continue;
1829
+ }
1830
+ value = value << 6 | idx;
1831
+ bits += 6;
1832
+ if (bits >= 8) {
1833
+ bits -= 8;
1834
+ out[o++] = value >> bits & 255;
1835
+ }
1836
+ }
1837
+ return out.slice(0, o);
1838
+ }
1839
+ function textOf(content) {
1840
+ if (typeof content === "string") {
1841
+ return content;
1842
+ }
1843
+ return content.filter((p) => p.type === "text").map((p) => p.text).join("\n\n");
1844
+ }
1845
+ function openAiContent(content) {
1846
+ if (typeof content === "string") {
1847
+ return content;
1848
+ }
1849
+ return content.map(
1850
+ (part) => part.type === "text" ? { type: "text", text: part.text } : { type: "image_url", image_url: { url: `data:${part.mime};base64,${bytesToBase64(part.bytes)}` } }
1851
+ );
1852
+ }
1853
+ function anthropicContent(content) {
1854
+ if (typeof content === "string") {
1855
+ return content;
1856
+ }
1857
+ return content.map(
1858
+ (part) => part.type === "text" ? { type: "text", text: part.text } : { type: "image", source: { type: "base64", media_type: part.mime, data: bytesToBase64(part.bytes) } }
1859
+ );
1860
+ }
1861
+ function buildCompletionBody(kind, model, req, opts = {}) {
1862
+ const stream = opts.stream ?? true;
1863
+ const body = kind === "anthropic" ? anthropicBody(model.id, req, stream) : chatCompletionsBody(kind, model.id, req, stream);
1864
+ const merged = mergeRequestOverrides(body, model.requestOverrides);
1865
+ const final = isPlainObject(req.requestOverrides) ? mergeRequestOverrides(merged, req.requestOverrides) : merged;
1866
+ return kind === "anthropic" ? final : acceptedOffEffort(kind, model.id, final);
1867
+ }
1868
+ function acceptedOffEffort(kind, modelId, body) {
1869
+ if (body.reasoning_effort !== "none") {
1870
+ return body;
1871
+ }
1872
+ const effort = openAiOffReasoningEffort(modelId, kind);
1873
+ return effort === null || effort === "none" ? body : { ...body, reasoning_effort: effort };
1874
+ }
1875
+ function chatCompletionsBody(kind, modelId, req, stream) {
1876
+ const body = {
1877
+ model: modelId,
1878
+ messages: req.messages.map((m) => ({ role: m.role, content: openAiContent(m.content) })),
1879
+ stream
1880
+ };
1881
+ if (req.maxTokens !== void 0) {
1882
+ body[kind === "openai" ? "max_completion_tokens" : "max_tokens"] = req.maxTokens;
1883
+ }
1884
+ if (req.temperature !== void 0 && !isOpenAiReasoningModel(modelId, kind)) {
1885
+ body.temperature = req.temperature;
1886
+ }
1887
+ if (req.format === "json") {
1888
+ body.response_format = kind === "lmstudio" ? { type: "json_schema", json_schema: { name: "response", schema: { type: "object" } } } : { type: "json_object" };
1889
+ }
1890
+ if (kind === "openai" && stream) {
1891
+ body.stream_options = { include_usage: true };
1892
+ }
1893
+ return body;
1894
+ }
1895
+ function anthropicBody(modelId, req, stream) {
1896
+ const system = req.messages.filter((m) => m.role === "system").map((m) => textOf(m.content));
1897
+ if (req.format === "json") {
1898
+ system.push(JSON_ONLY_INSTRUCTION);
1899
+ }
1900
+ const body = {
1901
+ model: modelId,
1902
+ max_tokens: req.maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
1903
+ messages: req.messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: anthropicContent(m.content) })),
1904
+ stream
1905
+ };
1906
+ if (system.length > 0) {
1907
+ body.system = system.join("\n\n");
1908
+ }
1909
+ return body;
1910
+ }
1911
+ var STREAM_USAGE_HOSTS = ["openrouter.ai", "generativelanguage.googleapis.com", "api.groq.com", "api.deepseek.com"];
1912
+ function streamUsageSupported(engine) {
1913
+ switch (engine.kind) {
1914
+ case "openai":
1915
+ case "lmstudio":
1916
+ case "ollama":
1917
+ return true;
1918
+ case "openai-compatible": {
1919
+ if (detectLocality(engine.baseUrl) === "local") {
1920
+ return true;
1921
+ }
1922
+ const host = hostOfUrl(engine.baseUrl);
1923
+ return host !== null && STREAM_USAGE_HOSTS.includes(host);
1924
+ }
1925
+ default:
1926
+ return false;
1927
+ }
1928
+ }
1929
+ function withStreamUsage(body, engine) {
1930
+ if (body.stream !== true || "stream_options" in body || !streamUsageSupported(engine)) {
1931
+ return body;
1932
+ }
1933
+ return { ...body, stream_options: { include_usage: true } };
1934
+ }
1935
+ var RETRYABLE_REQUEST_PARAMS = [
1936
+ "temperature",
1937
+ "top_p",
1938
+ "presence_penalty",
1939
+ "frequency_penalty",
1940
+ "max_tokens",
1941
+ "max_completion_tokens",
1942
+ "reasoning_effort",
1943
+ "stream_options"
1944
+ ];
1945
+ var UNSUPPORTED_WORDING = /unsupported|not supported|does not support|not compatible/i;
1946
+ function unsupportedRequestParam(status, bodyText) {
1947
+ if (status !== 400) {
1948
+ return null;
1949
+ }
1950
+ let message = bodyText;
1951
+ let param;
1952
+ let code;
1953
+ try {
1954
+ const json = JSON.parse(bodyText);
1955
+ const error = isPlainObject(json) ? json.error : void 0;
1956
+ if (isPlainObject(error)) {
1957
+ message = typeof error.message === "string" ? error.message : "";
1958
+ param = error.param;
1959
+ code = error.code;
1960
+ } else if (typeof error === "string") {
1961
+ message = error;
1962
+ } else if (isPlainObject(json) && typeof json.message === "string") {
1963
+ message = json.message;
1964
+ }
1965
+ } catch {
1966
+ }
1967
+ if (!UNSUPPORTED_WORDING.test(`${typeof code === "string" ? code : ""} ${message}`)) {
1968
+ return null;
1969
+ }
1970
+ if (typeof param === "string" && param !== "") {
1971
+ return RETRYABLE_REQUEST_PARAMS.includes(param) ? param : null;
1972
+ }
1973
+ for (const match of message.matchAll(/['"`]([a-z_]+)['"`]/g)) {
1974
+ if (RETRYABLE_REQUEST_PARAMS.includes(match[1])) {
1975
+ return match[1];
1976
+ }
1977
+ }
1978
+ return null;
1979
+ }
1980
+ function withoutRequestParam(body, param) {
1981
+ if (!(param in body)) {
1982
+ return null;
1983
+ }
1984
+ const out = { ...body };
1985
+ const value = out[param];
1986
+ delete out[param];
1987
+ const replacement = param === "max_tokens" ? "max_completion_tokens" : param === "max_completion_tokens" ? "max_tokens" : null;
1988
+ if (replacement && !(replacement in out)) {
1989
+ out[replacement] = value;
1990
+ }
1991
+ return out;
1992
+ }
1993
+ function buildEmbeddingsBody(kind, model, texts) {
1994
+ if (kind === "anthropic") {
1995
+ return null;
1996
+ }
1997
+ return mergeRequestOverrides({ model: model.id, input: texts }, model.requestOverrides);
1998
+ }
1999
+ function parseEmbeddingsResponse(body) {
2000
+ if (!isPlainObject(body) || !Array.isArray(body.data)) {
2001
+ return [];
2002
+ }
2003
+ return body.data.filter(isPlainObject).map((d, i) => ({ index: typeof d.index === "number" ? d.index : i, embedding: d.embedding })).sort((a, b) => a.index - b.index).map((d) => Array.isArray(d.embedding) ? d.embedding.filter((x) => typeof x === "number") : []);
2004
+ }
2005
+
2006
+ // src/ai/wire.ts
2007
+ function toWireCompletionRequest(req) {
2008
+ return {
2009
+ ...req,
2010
+ messages: req.messages.map((m) => ({
2011
+ role: m.role,
2012
+ content: typeof m.content === "string" ? m.content : m.content.map((p) => p.type === "text" ? p : { type: "image", mime: p.mime, dataBase64: bytesToBase64(p.bytes) })
2013
+ }))
2014
+ };
2015
+ }
2016
+ function fromWireCompletionRequest(wire) {
2017
+ return {
2018
+ ...wire,
2019
+ messages: wire.messages.map((m) => ({
2020
+ role: m.role,
2021
+ content: typeof m.content === "string" ? m.content : m.content.map((p) => p.type === "text" ? p : { type: "image", mime: p.mime, bytes: base64ToBytes(p.dataBase64) })
2022
+ }))
2023
+ };
2024
+ }
2025
+
2026
+ // src/ai/errors.ts
2027
+ var AI_ERROR_COPY = {
2028
+ unreachable: {
2029
+ title: "Engine not reachable",
2030
+ message: "The AI engine did not answer. Check that the server is running and the base URL is right.",
2031
+ action: "openSettings"
2032
+ },
2033
+ auth: {
2034
+ title: "Key rejected",
2035
+ message: "The engine refused the API key. Store a new key in AI settings, or check the environment variable.",
2036
+ action: "openSettings"
2037
+ },
2038
+ modelNotFound: {
2039
+ title: "Model not found",
2040
+ message: "The engine does not serve this model. Refresh models or pick another one in AI settings.",
2041
+ action: "openSettings"
2042
+ },
2043
+ modelLoadFailed: {
2044
+ title: "Model didn't load",
2045
+ message: "The engine couldn't load the model. Load it in the engine's app, or choose another model.",
2046
+ action: "chooseModel"
2047
+ },
2048
+ contextOverflow: {
2049
+ title: "Too much text",
2050
+ message: "The text is longer than the model's context window. Try a shorter selection.",
2051
+ action: "shortenSelection"
2052
+ },
2053
+ rateLimited: {
2054
+ title: "Rate limited",
2055
+ message: "The engine is busy or the rate limit was reached. Wait a moment and try again.",
2056
+ action: "retry"
2057
+ },
2058
+ cloudBlocked: {
2059
+ title: "Cloud engines are off",
2060
+ message: "This task is routed to a cloud engine, and Allow cloud is off in AI settings.",
2061
+ action: "openSettings"
2062
+ },
2063
+ consentDenied: {
2064
+ title: "Not allowed in this workspace",
2065
+ message: "Sending text to this cloud engine was not allowed for this workspace.",
2066
+ action: "openSettings"
2067
+ },
2068
+ cancelled: {
2069
+ title: "Cancelled",
2070
+ message: "The request was cancelled."
2071
+ },
2072
+ badOutput: {
2073
+ title: "No usable answer",
2074
+ message: "The model did not return a usable answer. Try again or pick another model.",
2075
+ action: "retry"
2076
+ },
2077
+ unknown: {
2078
+ title: "Something went wrong",
2079
+ message: "The AI request failed.",
2080
+ action: "retry"
2081
+ }
2082
+ };
2083
+ function aiLengthLimitCopy(opts) {
2084
+ if (opts.empty) {
2085
+ return opts.thinking ? { title: "No room left to answer", message: "The model used its whole length budget thinking and sent no answer. Lower Thinking, or Retry.", action: "retry" } : { title: "No room left to answer", message: "The model reached its length limit before it sent any text. Retry.", action: "retry" };
2086
+ }
2087
+ return opts.thinking ? { title: "Answer cut off", message: "The answer was cut off at the length limit. Lower Thinking, or Retry, for a full answer.", action: "retry" } : { title: "Answer cut off", message: "The answer was cut off at the length limit.", action: "retry" };
2088
+ }
2089
+ function isAiErrorCode(v) {
2090
+ return typeof v === "string" && AI_ERROR_CODES.includes(v);
2091
+ }
2092
+ function isAiError(v) {
2093
+ return typeof v === "object" && v !== null && isAiErrorCode(v.code) && typeof v.message === "string";
2094
+ }
2095
+ function makeAiError(code, message) {
2096
+ return { code, message: message ?? AI_ERROR_COPY[code].message };
2097
+ }
2098
+ function toAiError(err) {
2099
+ if (isAiError(err)) {
2100
+ return { code: err.code, message: err.message, ...typeof err.engineId === "string" ? { engineId: err.engineId } : {} };
2101
+ }
2102
+ if (typeof err === "object" && err !== null && err.name === "AbortError") {
2103
+ return makeAiError("cancelled");
2104
+ }
2105
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : AI_ERROR_COPY.unknown.message;
2106
+ return { code: "unknown", message };
2107
+ }
2108
+ var ENGINE_COPY = {
2109
+ unreachable: (engine) => `${engine} did not answer. Check that the server is running and the base URL is right.`,
2110
+ auth: (engine) => `${engine} refused the API key. Store a new key, or check the environment variable.`,
2111
+ modelNotFound: (engine) => `${engine} does not serve this model. Refresh its models or pick another one.`,
2112
+ modelLoadFailed: (engine, model) => `${engine} couldn't load ${model}. Load it in ${engine}, or choose another model.`
2113
+ };
2114
+ function aiErrorCopy(code, opts = {}) {
2115
+ const copy = AI_ERROR_COPY[code] ?? AI_ERROR_COPY.unknown;
2116
+ const named = opts.engine ? ENGINE_COPY[code] : void 0;
2117
+ if (!named || !opts.engine) {
2118
+ return copy;
2119
+ }
2120
+ return { ...copy, message: named(opts.engine, opts.model || "the model"), action: copy.action === "chooseModel" ? "chooseModel" : "openEngine" };
2121
+ }
2122
+ var MODEL_LOAD_FAILED_PHRASES = [
2123
+ "failed to load model",
2124
+ "error loading model",
2125
+ "unable to load model",
2126
+ "could not load model",
2127
+ "couldn't load model",
2128
+ "model failed to load",
2129
+ "llama runner process has terminated",
2130
+ "requires more system memory"
2131
+ ];
2132
+ function isModelLoadFailure(bodyText) {
2133
+ const lower = bodyText.toLowerCase();
2134
+ return MODEL_LOAD_FAILED_PHRASES.some((p) => lower.includes(p));
2135
+ }
2136
+ function aiErrorCodeFromHttp(status, bodyText = "") {
2137
+ if (status === 401 || status === 403) {
2138
+ return "auth";
2139
+ }
2140
+ if (status === 404) {
2141
+ return "modelNotFound";
2142
+ }
2143
+ if (isModelLoadFailure(bodyText)) {
2144
+ return "modelLoadFailed";
2145
+ }
2146
+ if (status === 413) {
2147
+ return "contextOverflow";
2148
+ }
2149
+ if (status === 429 || status === 529) {
2150
+ return "rateLimited";
2151
+ }
2152
+ if (status === 400 && /context|too long|maximum.*tokens|token limit/i.test(bodyText)) {
2153
+ return "contextOverflow";
2154
+ }
2155
+ if (status === 400 && /model.*(not found|does not exist|unknown)|no such model/i.test(bodyText)) {
2156
+ return "modelNotFound";
2157
+ }
2158
+ if (status === 502 || status === 503 || status === 504) {
2159
+ return "unreachable";
2160
+ }
2161
+ return "unknown";
2162
+ }
2163
+
2164
+ // src/ai/thinking.ts
2165
+ var TIERS = ["low", "medium", "high"];
2166
+ var LABELS = { off: "Off", low: "Low", medium: "Medium", high: "High" };
2167
+ function isObject3(v) {
2168
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2169
+ }
2170
+ function thinkingChoices(kind) {
2171
+ switch (kind) {
2172
+ case "openai":
2173
+ case "openai-compatible":
2174
+ case "ollama":
2175
+ return ["off", ...TIERS].map((level) => ({ level, label: LABELS[level] }));
2176
+ case "lmstudio":
2177
+ return [{ level: "off", label: "Off" }, { level: "medium", label: "On" }];
2178
+ case "anthropic":
2179
+ return TIERS.map((level) => ({ level, label: LABELS[level] }));
2180
+ // `vscode.lm` takes no thinking fields, so the dial hides.
2181
+ case "vscode-lm":
2182
+ default:
2183
+ return [];
2184
+ }
2185
+ }
2186
+ function nearestThinkingLevel(kind, level) {
2187
+ const choices = thinkingChoices(kind);
2188
+ if (!level || choices.length === 0) {
2189
+ return null;
2190
+ }
2191
+ if (choices.some((c) => c.level === level)) {
2192
+ return level;
2193
+ }
2194
+ return level === "off" ? choices[0].level : (choices.find((c) => c.level !== "off") ?? choices[0]).level;
2195
+ }
2196
+ var SHORT_EDIT_JOBS = /* @__PURE__ */ new Set(["rewrite", "translate", "title", "frontmatter"]);
2197
+ function taskThinkingDefault(job) {
2198
+ return job && SHORT_EDIT_JOBS.has(job) ? "off" : null;
2199
+ }
2200
+ function resolveThinkingLevel(kind, explicit, configured, job, opts = {}) {
2201
+ if (explicit) {
2202
+ return { level: explicit, auto: false };
2203
+ }
2204
+ if (configured) {
2205
+ return { level: null, auto: false };
2206
+ }
2207
+ if (kind === "openai" && opts.modelId !== void 0 && !isOpenAiReasoningModel(opts.modelId, "openai")) {
2208
+ return { level: null, auto: false };
2209
+ }
2210
+ const level = nearestThinkingLevel(kind, taskThinkingDefault(job));
2211
+ return { level, auto: level !== null };
2212
+ }
2213
+ function thinkingOverrides(kind, level, opts = {}) {
2214
+ if (!thinkingChoices(kind).some((c) => c.level === level)) {
2215
+ return {};
2216
+ }
2217
+ switch (kind) {
2218
+ case "anthropic":
2219
+ return { thinking: { type: "adaptive" }, output_config: { effort: level } };
2220
+ case "openai-compatible":
2221
+ if (level === "off" && opts.local) {
2222
+ return { chat_template_kwargs: { enable_thinking: false } };
2223
+ }
2224
+ return { reasoning_effort: level === "off" ? "none" : level };
2225
+ default:
2226
+ return { reasoning_effort: level === "off" ? "none" : level };
2227
+ }
2228
+ }
2229
+ function thinkingLevelFrom(kind, overrides) {
2230
+ if (!overrides || thinkingChoices(kind).length === 0) {
2231
+ return null;
2232
+ }
2233
+ if (kind === "anthropic") {
2234
+ const effort2 = isObject3(overrides.output_config) ? overrides.output_config.effort : void 0;
2235
+ return typeof effort2 === "string" && TIERS.includes(effort2) ? effort2 : null;
2236
+ }
2237
+ const kwargs = isObject3(overrides.chat_template_kwargs) ? overrides.chat_template_kwargs : void 0;
2238
+ if (kwargs?.enable_thinking === false) {
2239
+ return "off";
2240
+ }
2241
+ const effort = overrides.reasoning_effort;
2242
+ if (effort === "none") {
2243
+ return "off";
2244
+ }
2245
+ if (typeof effort !== "string" || !TIERS.includes(effort)) {
2246
+ return null;
2247
+ }
2248
+ return kind === "lmstudio" ? "medium" : effort;
2249
+ }
2250
+ function stripThinking(kind, overrides) {
2251
+ const out = { ...overrides };
2252
+ const dropNested = (key, inner) => {
2253
+ const current = out[key];
2254
+ if (!isObject3(current)) {
2255
+ return;
2256
+ }
2257
+ const rest = { ...current };
2258
+ delete rest[inner];
2259
+ if (Object.keys(rest).length === 0) {
2260
+ delete out[key];
2261
+ } else {
2262
+ out[key] = rest;
2263
+ }
2264
+ };
2265
+ if (kind === "anthropic") {
2266
+ delete out.thinking;
2267
+ dropNested("output_config", "effort");
2268
+ } else {
2269
+ delete out.reasoning_effort;
2270
+ dropNested("chat_template_kwargs", "enable_thinking");
2271
+ }
2272
+ return out;
2273
+ }
2274
+ function deepMerge(base, patch) {
2275
+ const out = { ...base };
2276
+ for (const [key, value] of Object.entries(patch)) {
2277
+ const current = out[key];
2278
+ out[key] = isObject3(current) && isObject3(value) ? deepMerge(current, value) : value;
2279
+ }
2280
+ return out;
2281
+ }
2282
+ function withThinkingLevel(kind, overrides, level, opts = {}) {
2283
+ const base = stripThinking(kind, overrides ?? {});
2284
+ const next = level ? deepMerge(base, thinkingOverrides(kind, level, opts)) : base;
2285
+ return Object.keys(next).length > 0 ? next : void 0;
2286
+ }
2287
+ var THINKING_HEADROOM_TOKENS = { off: 0, low: 2048, medium: 4096, high: 8192 };
2288
+ var THINKING_DEFAULT_HEADROOM_TOKENS = THINKING_HEADROOM_TOKENS.medium;
2289
+ var THINKING_MAX_TOKENS_CAP = 12288;
2290
+ function thinkingMaxTokens(answerTokens, level, opts = {}) {
2291
+ const headroom = level ? THINKING_HEADROOM_TOKENS[level] : THINKING_DEFAULT_HEADROOM_TOKENS;
2292
+ if (headroom <= 0) {
2293
+ return answerTokens;
2294
+ }
2295
+ let limit = THINKING_MAX_TOKENS_CAP;
2296
+ if (opts.contextTokens && opts.contextTokens > 0) {
2297
+ limit = Math.min(limit, opts.contextTokens - Math.ceil(opts.promptTokens ?? 0) - 256);
2298
+ }
2299
+ return Math.max(answerTokens, Math.min(answerTokens + headroom, limit));
2300
+ }
2301
+
2302
+ // src/ai/prompts/rewrite.ts
2303
+ var AI_AUDIENCES = [
2304
+ {
2305
+ id: "general",
2306
+ label: "General audience",
2307
+ instruction: "Rewrite it for a general audience: plain words, short sentences, jargon explained or avoided. Keep the meaning and every fact, name and link."
2308
+ },
2309
+ {
2310
+ id: "expert",
2311
+ label: "Expert",
2312
+ instruction: "Rewrite it for expert readers: precise terminology, no basic explanations, dense and direct. Keep the meaning and every fact, name and link."
2313
+ },
2314
+ {
2315
+ id: "plain",
2316
+ label: "Plain language",
2317
+ instruction: "Rewrite it in plain language: everyday words, short sentences, active voice, no jargon or idioms. Keep the meaning and every fact, name and link."
2318
+ },
2319
+ {
2320
+ id: "grade8",
2321
+ label: "8th grade",
2322
+ instruction: "Rewrite it at an 8th grade reading level: common words, short sentences, one idea at a time. Keep the meaning and every fact, name and link."
2323
+ },
2324
+ {
2325
+ id: "esl",
2326
+ label: "English learners (ESL)",
2327
+ instruction: "Rewrite it for readers who are learning English: simple vocabulary, short direct sentences, literal wording, no idioms or phrasal verbs. Keep the meaning and every fact, name and link."
2328
+ }
2329
+ ];
2330
+ function getAiAudience(id) {
2331
+ return AI_AUDIENCES.find((a) => a.id === id);
2332
+ }
2333
+ var AI_REWRITE_VARIANTS = [
2334
+ {
2335
+ id: "shorter",
2336
+ label: "Shorter",
2337
+ instruction: "Make it shorter and tighter. Keep the meaning and every fact, name and link; cut repetition and filler.",
2338
+ group: "length",
2339
+ output: "replace"
2340
+ },
2341
+ {
2342
+ id: "longer",
2343
+ label: "Longer",
2344
+ title: "Longer: the same points, said at more length",
2345
+ instruction: "Make it longer: add detail, examples or explanation that fit the surrounding text. Do not invent facts, numbers, names or links.",
2346
+ group: "length",
2347
+ output: "replace"
2348
+ },
2349
+ {
2350
+ id: "expand",
2351
+ label: "Add detail",
2352
+ title: "Add detail (facts and examples)",
2353
+ instruction: "Expand it: develop each point with the reasoning, detail or an example that the surrounding text supports, so it reads as a fuller passage. Do not invent facts, numbers, names or links.",
2354
+ group: "length",
2355
+ output: "replace"
2356
+ },
2357
+ {
2358
+ id: "simpler",
2359
+ label: "Simpler",
2360
+ instruction: "Make it simpler: shorter sentences, everyday words, one idea at a time. Keep the meaning and every fact, name and link.",
2361
+ group: "tone",
2362
+ output: "replace"
2363
+ },
2364
+ {
2365
+ id: "fixFlow",
2366
+ label: "Fix flow",
2367
+ instruction: "Fix the flow: correct grammar, spelling and punctuation, and smooth awkward sentences and transitions. Keep the length, meaning and tone close to the original.",
2368
+ group: "tone",
2369
+ output: "replace"
2370
+ },
2371
+ {
2372
+ id: "formal",
2373
+ label: "More formal",
2374
+ instruction: "Make the tone more formal and professional: no contractions or slang, precise wording. Keep the meaning, every fact and the length close to the original.",
2375
+ group: "tone",
2376
+ output: "replace"
2377
+ },
2378
+ {
2379
+ id: "casual",
2380
+ label: "More casual",
2381
+ instruction: "Make the tone more casual and conversational: contractions and everyday phrasing. Keep the meaning, every fact and the length close to the original.",
2382
+ group: "tone",
2383
+ output: "replace"
2384
+ },
2385
+ {
2386
+ id: "readingLevel",
2387
+ label: "Reading level\u2026",
2388
+ instruction: "{audience}",
2389
+ group: "audience",
2390
+ output: "replace",
2391
+ param: "audience"
2392
+ },
2393
+ {
2394
+ id: "translate",
2395
+ label: "Translate\u2026",
2396
+ instruction: "Translate the passage into {language}.",
2397
+ group: "translate",
2398
+ output: "replace",
2399
+ param: "language"
2400
+ },
2401
+ {
2402
+ id: "explain",
2403
+ label: "Explain",
2404
+ instruction: "Explain what the passage says and means in plain words, as a short note for the reader. Do not repeat the passage.",
2405
+ group: "explain",
2406
+ output: "readOnly"
2407
+ },
2408
+ { id: "custom", label: "Custom prompt\u2026", instruction: "", group: "custom", output: "replace" }
2409
+ ];
2410
+ function getAiRewriteVariant(id) {
2411
+ return AI_REWRITE_VARIANTS.find((v) => v.id === id);
2412
+ }
2413
+ var REWRITE_CONTEXT_CHARS = 1600;
2414
+ var REWRITE_INSTRUCTION_MAX_CHARS = 1e3;
2415
+ var REWRITE_LANGUAGE_MAX_CHARS = 60;
2416
+ var REWRITE_SYSTEM_PROMPT = [
2417
+ "You are a writing assistant inside a Markdown editor. You rewrite the passage the user selected, following one instruction.",
2418
+ "",
2419
+ "Rules:",
2420
+ "- Reply with the rewritten passage only. No preamble, no explanation, no quotation marks and no code fence around it.",
2421
+ "- Keep it Markdown, and keep the structure of the passage: headings stay headings, list items stay list items with the same markers and indentation, tables stay tables, quotes stay quotes. A passage that is part of a sentence stays part of a sentence.",
2422
+ "- Keep every link and image with its exact URL, inline code and code blocks character for character, and inline formatting (bold, italic, strikethrough, ==highlight==, footnote references, [[wikilinks]], #tags, math) on the words it belongs to.",
2423
+ "- Write in the language and voice of the passage.",
2424
+ "- The text before and after the passage is context only. Do not repeat it and do not continue it.",
2425
+ "- If the instruction asks for anything other than a version of the passage, still reply with a version of the passage only."
2426
+ ].join("\n");
2427
+ var TRANSLATE_SYSTEM_ADDENDUM = [
2428
+ 'This is a translation. The "write in the language and voice of the passage" rule above does not apply: write in the language named in the instruction.',
2429
+ "- Translate prose only: paragraph text, list item text, table cell text, heading text, image alt text and link text.",
2430
+ "- Keep verbatim, untranslated: the Markdown syntax itself (heading marks, list markers and indentation, table pipes and separator rows, quote marks), link and image URLs, inline code and fenced code blocks (including their language tag), [[wikilinks]], #tags, math, footnote markers such as [^1], HTML tags and frontmatter keys.",
2431
+ "- Keep the line structure: the same number of lines, blocks and list items in the same order, with blank lines where the passage has them.",
2432
+ "- Keep names of people, products and places as they are unless they have a well established translation."
2433
+ ].join("\n");
2434
+ var EXPLAIN_SYSTEM_ADDENDUM = 'The instruction asks for an explanation of the passage, not a version of it: the "version of the passage" rules above do not apply. Reply with the explanation only, as Markdown prose, and do not repeat the passage.';
2435
+ var TRANSLATE_DOCUMENT_NOTE = "The passage is a whole document body (its frontmatter has been removed and is handled elsewhere). Translate all of it, from the first line to the last.";
2436
+ function escapeClosingTag(text, tag) {
2437
+ return text.split(`</${tag}>`).join(`<\\/${tag}>`);
2438
+ }
2439
+ function trimRewriteContext(text, side, max = REWRITE_CONTEXT_CHARS) {
2440
+ if (text.length <= max) {
2441
+ return text;
2442
+ }
2443
+ if (side === "before") {
2444
+ const cut2 = text.slice(text.length - max);
2445
+ const nl2 = cut2.indexOf("\n");
2446
+ return nl2 >= 0 && nl2 < max / 2 ? cut2.slice(nl2 + 1) : cut2;
2447
+ }
2448
+ const cut = text.slice(0, max);
2449
+ const nl = cut.lastIndexOf("\n");
2450
+ return nl > max / 2 ? cut.slice(0, nl) : cut;
2451
+ }
2452
+ function normalizeRewriteLanguage(language) {
2453
+ return (language ?? "").replace(/[<>\r\n]+/g, " ").replace(/\s+/g, " ").trim().slice(0, REWRITE_LANGUAGE_MAX_CHARS);
2454
+ }
2455
+ function rewriteInstruction(input) {
2456
+ const fallback = getAiRewriteVariant("fixFlow")?.instruction ?? "";
2457
+ if (input.variant === "custom") {
2458
+ const own = (input.instruction ?? "").trim().slice(0, REWRITE_INSTRUCTION_MAX_CHARS);
2459
+ return own || fallback;
2460
+ }
2461
+ const def = getAiRewriteVariant(input.variant);
2462
+ if (!def) {
2463
+ return "";
2464
+ }
2465
+ if (def.param === "language") {
2466
+ const language = normalizeRewriteLanguage(input.params?.language);
2467
+ return language ? def.instruction.replace("{language}", () => language) : fallback;
2468
+ }
2469
+ if (def.param === "audience") {
2470
+ const audience = input.params?.audience ? getAiAudience(input.params.audience) : void 0;
2471
+ return audience ? def.instruction.replace("{audience}", () => audience.instruction) : fallback;
2472
+ }
2473
+ return def.instruction;
2474
+ }
2475
+ function rewriteSystemPrompt(variant, scope = "selection") {
2476
+ if (variant === "translate") {
2477
+ return [REWRITE_SYSTEM_PROMPT, TRANSLATE_SYSTEM_ADDENDUM, ...scope === "document" ? [TRANSLATE_DOCUMENT_NOTE] : []].join("\n\n");
2478
+ }
2479
+ if (variant === "explain") {
2480
+ return `${REWRITE_SYSTEM_PROMPT}
2481
+
2482
+ ${EXPLAIN_SYSTEM_ADDENDUM}`;
2483
+ }
2484
+ return REWRITE_SYSTEM_PROMPT;
2485
+ }
2486
+ function buildRewriteMessages(input) {
2487
+ const instruction = rewriteInstruction(input);
2488
+ const scope = input.scope === "document" && input.variant === "translate" ? "document" : "selection";
2489
+ const before = scope === "document" ? "" : trimRewriteContext(input.before ?? "", "before").trimEnd();
2490
+ const after = scope === "document" ? "" : trimRewriteContext(input.after ?? "", "after").trimStart();
2491
+ const parts = [];
2492
+ parts.push(input.variant === "custom" ? `Instruction (from the user): <instruction>
2493
+ ${escapeClosingTag(instruction, "instruction")}
2494
+ </instruction>` : `Instruction: ${instruction}`);
2495
+ if (before) {
2496
+ parts.push(`Text before the passage (context only, do not rewrite it):
2497
+ <before>
2498
+ ${escapeClosingTag(before, "before")}
2499
+ </before>`);
2500
+ }
2501
+ parts.push(`${input.variant === "explain" ? "Passage to explain" : "Passage to rewrite"}:
2502
+ <passage>
2503
+ ${escapeClosingTag(input.selection, "passage")}
2504
+ </passage>`);
2505
+ if (after) {
2506
+ parts.push(`Text after the passage (context only, do not rewrite it):
2507
+ <after>
2508
+ ${escapeClosingTag(after, "after")}
2509
+ </after>`);
2510
+ }
2511
+ parts.push(input.variant === "explain" ? "Reply with the explanation only." : "Reply with the rewritten passage only.");
2512
+ return [
2513
+ { role: "system", content: rewriteSystemPrompt(input.variant, scope) },
2514
+ { role: "user", content: parts.join("\n\n") }
2515
+ ];
2516
+ }
2517
+ var EXPLAIN_MAX_TOKENS = 600;
2518
+ var REWRITE_TOKEN_FACTOR = {
2519
+ longer: 3,
2520
+ expand: 3,
2521
+ translate: 3,
2522
+ custom: 3,
2523
+ simpler: 1.8,
2524
+ formal: 1.8,
2525
+ casual: 1.8
2526
+ };
2527
+ function rewriteMaxTokens(selection, variant) {
2528
+ if (variant === "explain") {
2529
+ return EXPLAIN_MAX_TOKENS;
2530
+ }
2531
+ const approxTokens = Math.ceil(selection.length / 3.5);
2532
+ const factor = REWRITE_TOKEN_FACTOR[variant] ?? 1.6;
2533
+ return Math.max(256, Math.min(4096, Math.ceil(approxTokens * factor) + 64));
2534
+ }
2535
+ function rewriteVisibleText(raw) {
2536
+ let text = raw.replace(/<think>[\s\S]*?<\/think>\s*/g, "");
2537
+ const open = text.indexOf("<think>");
2538
+ if (open >= 0) {
2539
+ text = text.slice(0, open);
2540
+ }
2541
+ return text.replace(/^\s*<passage>\s*\n?/, "").replace(/\n?\s*<\/passage>\s*$/, "");
2542
+ }
2543
+ function finalizeRewriteText(raw, selection) {
2544
+ let text = rewriteVisibleText(raw).trim();
2545
+ const fenced = /^```(?:markdown|md|text)?[ \t]*\n([\s\S]*?)\n```$/i.exec(text);
2546
+ if (fenced && !selection.trimStart().startsWith("```")) {
2547
+ text = fenced[1].trim();
2548
+ }
2549
+ const quoted = /^(["“])([\s\S]*)(["”])$/.exec(text);
2550
+ if (quoted && !/^\s*["“]/.test(selection) && !quoted[2].includes(quoted[1])) {
2551
+ text = quoted[2].trim();
2552
+ }
2553
+ if (!text) {
2554
+ return "";
2555
+ }
2556
+ const lead = /^\s*/.exec(selection)?.[0] ?? "";
2557
+ const trail = /\s*$/.exec(selection)?.[0] ?? "";
2558
+ return lead + text + trail;
2559
+ }
2560
+
2561
+ // src/ai/prompts/explain.ts
2562
+ var EXPLAIN_SYSTEM_PROMPT = [
2563
+ "You are a writing assistant inside a Markdown editor. The user selected a passage and wants it explained.",
2564
+ "",
2565
+ "Rules:",
2566
+ "- Reply with the explanation only, as short Markdown prose (a few sentences or a short paragraph, a brief list only when the passage has several distinct points). No preamble, no heading, and no code fence around the whole reply.",
2567
+ "- Explain what the passage says and means, and define any term a reader may not know. Do not repeat or quote the passage back and do not rewrite it.",
2568
+ "- Do not invent facts. If the passage or its context is not enough to explain something, say so in one sentence.",
2569
+ "- Write in the language of the passage.",
2570
+ "- The text before and after the passage and the document title are context only. Do not explain them."
2571
+ ].join("\n");
2572
+ function buildExplainMessages(input) {
2573
+ const before = trimRewriteContext(input.before ?? "", "before").trimEnd();
2574
+ const after = trimRewriteContext(input.after ?? "", "after").trimStart();
2575
+ const title = (input.title ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
2576
+ const parts = [`Instruction: ${getAiRewriteVariant("explain")?.instruction ?? ""}`];
2577
+ if (title) {
2578
+ parts.push(`Document title (context only): ${title}`);
2579
+ }
2580
+ if (before) {
2581
+ parts.push(`Text before the passage (context only, do not explain it):
2582
+ <before>
2583
+ ${escapeClosingTag(before, "before")}
2584
+ </before>`);
2585
+ }
2586
+ parts.push(`Passage to explain:
2587
+ <passage>
2588
+ ${escapeClosingTag(input.selection, "passage")}
2589
+ </passage>`);
2590
+ if (after) {
2591
+ parts.push(`Text after the passage (context only, do not explain it):
2592
+ <after>
2593
+ ${escapeClosingTag(after, "after")}
2594
+ </after>`);
2595
+ }
2596
+ parts.push("Reply with the explanation only.");
2597
+ return [
2598
+ { role: "system", content: EXPLAIN_SYSTEM_PROMPT },
2599
+ { role: "user", content: parts.join("\n\n") }
2600
+ ];
2601
+ }
2602
+ function explainMaxTokens() {
2603
+ return EXPLAIN_MAX_TOKENS;
2604
+ }
2605
+
2606
+ // src/ai/prompts/structureCheck.ts
2607
+ var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
2608
+ var HEADING = /^ {0,3}(#{1,6})(?:[ \t]|$)/;
2609
+ var LIST_ITEM = /^[ \t]*(?:[-*+]|\d{1,9}[.)])(?:[ \t]|$)/;
2610
+ var TABLE_DELIMITER = /^[ \t]*\|?[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/;
2611
+ var FOOTNOTE_DEFINITION = /^ {0,3}\[\^[^\]\s]+\]:/;
2612
+ var URL_PART = String.raw`<?((?:[^()\s<>]|\([^()\s]*\))+)>?`;
2613
+ var LINK_TEXT = String.raw`\[(?:[^\[\]]|\[[^\[\]]*\])*\]`;
2614
+ var INLINE_IMAGE = new RegExp(String.raw`!${LINK_TEXT}\(\s*${URL_PART}(?:\s+(?:"[^"]*"|'[^']*'|\([^)]*\)))?\s*\)`, "g");
2615
+ var INLINE_LINK = new RegExp(String.raw`(?<!!)${LINK_TEXT}\(\s*${URL_PART}(?:\s+(?:"[^"]*"|'[^']*'|\([^)]*\)))?\s*\)`, "g");
2616
+ var REFERENCE_DEFINITION = /^ {0,3}\[(?!\^)[^\]]+\]:[ \t]*<?(\S+?)>?(?:[ \t]+["'(].*)?$/gm;
2617
+ var AUTOLINK = /<([a-z][a-z0-9+.-]*:[^\s<>]+)>/gi;
2618
+ var WIKILINK = /\[\[[^\]\n]+\]\]/g;
2619
+ var FOOTNOTE_REFERENCE = /\[\^[^\]\s]+\]/g;
2620
+ function splitFences(markdown) {
2621
+ const prose = [];
2622
+ let fenced = 0;
2623
+ let open = null;
2624
+ for (const line of markdown.split(/\r?\n/)) {
2625
+ if (open) {
2626
+ const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line);
2627
+ if (close && close[1][0] === open.char && close[1].length >= open.length) {
2628
+ open = null;
2629
+ }
2630
+ continue;
2631
+ }
2632
+ const m = FENCE_OPEN.exec(line);
2633
+ if (m && !(m[1][0] === "`" && line.slice(m[0].length).includes("`"))) {
2634
+ open = { char: m[1][0], length: m[1].length };
2635
+ fenced++;
2636
+ continue;
2637
+ }
2638
+ prose.push(line);
2639
+ }
2640
+ return { prose, fenced };
2641
+ }
2642
+ function stripInlineCode(text) {
2643
+ let count = 0;
2644
+ const out = text.replace(/(`+)(?!`)([\s\S]*?[^`])\1(?!`)/g, () => {
2645
+ count++;
2646
+ return "";
2647
+ });
2648
+ return { text: out, count };
2649
+ }
2650
+ function tableRowCount(lines) {
2651
+ let rows = 0;
2652
+ for (let i = 1; i < lines.length; i++) {
2653
+ const header = lines[i - 1];
2654
+ if (!lines[i].includes("|") || !lines[i].includes("-") || !TABLE_DELIMITER.test(lines[i]) || !header.includes("|")) {
2655
+ continue;
2656
+ }
2657
+ rows++;
2658
+ let j = i + 1;
2659
+ while (j < lines.length && lines[j].trim() !== "" && lines[j].includes("|")) {
2660
+ rows++;
2661
+ j++;
2662
+ }
2663
+ i = j - 1;
2664
+ }
2665
+ return rows;
2666
+ }
2667
+ function collect(re, text) {
2668
+ const urls = [];
2669
+ for (const m of text.matchAll(re)) {
2670
+ urls.push(m[1]);
2671
+ }
2672
+ return urls;
2673
+ }
2674
+ function countMarkdownStructure(markdown) {
2675
+ const { prose, fenced } = splitFences(markdown);
2676
+ const headings = [0, 0, 0, 0, 0, 0];
2677
+ let listItems = 0;
2678
+ for (const line of prose) {
2679
+ const h = HEADING.exec(line);
2680
+ if (h) {
2681
+ headings[h[1].length - 1]++;
2682
+ } else if (LIST_ITEM.test(line)) {
2683
+ listItems++;
2684
+ }
2685
+ }
2686
+ const tableRows = tableRowCount(prose);
2687
+ const code = stripInlineCode(prose.map((l) => l.replace(FOOTNOTE_DEFINITION, "")).join("\n"));
2688
+ const images = collect(INLINE_IMAGE, code.text);
2689
+ const withoutImages = code.text.replace(INLINE_IMAGE, "");
2690
+ const links = [
2691
+ ...collect(INLINE_LINK, withoutImages),
2692
+ ...collect(REFERENCE_DEFINITION, withoutImages),
2693
+ ...collect(AUTOLINK, withoutImages)
2694
+ ];
2695
+ return {
2696
+ headings,
2697
+ fencedCodeBlocks: fenced,
2698
+ inlineCodeSpans: code.count,
2699
+ links,
2700
+ images,
2701
+ listItems,
2702
+ tableRows,
2703
+ wikilinks: (code.text.match(WIKILINK) ?? []).length,
2704
+ footnoteReferences: (code.text.match(FOOTNOTE_REFERENCE) ?? []).length
2705
+ };
2706
+ }
2707
+ function noun(n, singular, plural = `${singular}s`) {
2708
+ return `${n} ${n === 1 ? singular : plural}`;
2709
+ }
2710
+ function countMessage(original, result, singular, plural) {
2711
+ return `${noun(original, singular, plural)} in the original, ${result} in the result`;
2712
+ }
2713
+ function missingUrls(from, against) {
2714
+ const left = /* @__PURE__ */ new Map();
2715
+ for (const u of against) {
2716
+ left.set(u, (left.get(u) ?? 0) + 1);
2717
+ }
2718
+ const missing = [];
2719
+ for (const u of from) {
2720
+ const n = left.get(u) ?? 0;
2721
+ if (n > 0) {
2722
+ left.set(u, n - 1);
2723
+ } else {
2724
+ missing.push(u);
2725
+ }
2726
+ }
2727
+ return missing;
2728
+ }
2729
+ var URL_LIST_CAP = 3;
2730
+ function urlList(urls) {
2731
+ const shown = urls.slice(0, URL_LIST_CAP).join(", ");
2732
+ return urls.length > URL_LIST_CAP ? `${shown} and ${urls.length - URL_LIST_CAP} more` : shown;
2733
+ }
2734
+ function urlMismatch(kind, original, result) {
2735
+ const missing = missingUrls(original, result);
2736
+ const extra = missingUrls(result, original);
2737
+ if (original.length === result.length && !missing.length) {
2738
+ return null;
2739
+ }
2740
+ const plural = `${kind}s`;
2741
+ let message;
2742
+ if (original.length !== result.length) {
2743
+ message = countMessage(original.length, result.length, kind, plural);
2744
+ if (missing.length) {
2745
+ message += ` (missing: ${urlList(missing)})`;
2746
+ }
2747
+ } else {
2748
+ message = `${noun(original.length, kind, plural)} point somewhere else in the result (missing: ${urlList(missing)}; unexpected: ${urlList(extra)})`;
2749
+ }
2750
+ return { kind, original: original.length, result: result.length, message };
2751
+ }
2752
+ function checkMarkdownStructure(original, result) {
2753
+ const a = countMarkdownStructure(original);
2754
+ const b = countMarkdownStructure(result);
2755
+ const mismatches = [];
2756
+ const count = (kind, x, y, singular, plural) => {
2757
+ if (x !== y) {
2758
+ mismatches.push({ kind, original: x, result: y, message: countMessage(x, y, singular, plural) });
2759
+ }
2760
+ };
2761
+ a.headings.forEach((n, i) => count("heading", n, b.headings[i], `level ${i + 1} heading`));
2762
+ count("fencedCodeBlock", a.fencedCodeBlocks, b.fencedCodeBlocks, "fenced code block");
2763
+ count("inlineCode", a.inlineCodeSpans, b.inlineCodeSpans, "inline code span");
2764
+ for (const m of [urlMismatch("link", a.links, b.links), urlMismatch("image", a.images, b.images)]) {
2765
+ if (m) {
2766
+ mismatches.push(m);
2767
+ }
2768
+ }
2769
+ count("listItem", a.listItems, b.listItems, "list item");
2770
+ count("tableRow", a.tableRows, b.tableRows, "table row");
2771
+ count("wikilink", a.wikilinks, b.wikilinks, "wikilink");
2772
+ count("footnote", a.footnoteReferences, b.footnoteReferences, "footnote reference");
2773
+ return { ok: mismatches.length === 0, mismatches, messages: mismatches.map((m) => m.message) };
2774
+ }
2775
+
2776
+ // src/ai/json.ts
2777
+ function stripReasoning(raw) {
2778
+ let text = raw.replace(/<think>[\s\S]*?<\/think>/gi, "");
2779
+ const open = text.search(/<think>/i);
2780
+ if (open >= 0) {
2781
+ text = text.slice(0, open);
2782
+ }
2783
+ return text.trim();
2784
+ }
2785
+ function fencedBodies(text) {
2786
+ const out = [];
2787
+ const re = /```[A-Za-z0-9_-]*[ \t]*\r?\n([\s\S]*?)(?:\r?\n```|$)/g;
2788
+ let m;
2789
+ while ((m = re.exec(text)) !== null) {
2790
+ out.push(m[1]);
2791
+ if (m[0].length === 0) {
2792
+ re.lastIndex++;
2793
+ }
2794
+ }
2795
+ return out;
2796
+ }
2797
+ function balancedSpan(text, start) {
2798
+ const stack = [];
2799
+ let quote = null;
2800
+ for (let i = start; i < text.length; i++) {
2801
+ const c = text[i];
2802
+ if (quote) {
2803
+ if (c === "\\") {
2804
+ i++;
2805
+ } else if (c === quote) {
2806
+ quote = null;
2807
+ }
2808
+ continue;
2809
+ }
2810
+ if (c === '"' || c === "'") {
2811
+ quote = c;
2812
+ } else if (c === "{") {
2813
+ stack.push("}");
2814
+ } else if (c === "[") {
2815
+ stack.push("]");
2816
+ } else if (c === "}" || c === "]") {
2817
+ if (stack.pop() !== c) {
2818
+ return null;
2819
+ }
2820
+ if (stack.length === 0) {
2821
+ return { end: i + 1 };
2822
+ }
2823
+ }
2824
+ }
2825
+ return null;
2826
+ }
2827
+ function repairJson(src) {
2828
+ let out = "";
2829
+ let i = 0;
2830
+ let prev = "";
2831
+ while (i < src.length) {
2832
+ const c = src[i];
2833
+ if (c === '"' || c === "'") {
2834
+ const q = c;
2835
+ let body = "";
2836
+ i++;
2837
+ while (i < src.length && src[i] !== q) {
2838
+ const ch = src[i];
2839
+ if (ch === "\\" && i + 1 < src.length) {
2840
+ const next = src[i + 1];
2841
+ body += next === "'" ? "'" : ch + next;
2842
+ i += 2;
2843
+ continue;
2844
+ }
2845
+ if (ch === "\n") {
2846
+ body += "\\n";
2847
+ } else if (ch === "\r") {
2848
+ body += "\\r";
2849
+ } else if (ch === " ") {
2850
+ body += "\\t";
2851
+ } else if (ch === '"') {
2852
+ body += '\\"';
2853
+ } else {
2854
+ body += ch;
2855
+ }
2856
+ i++;
2857
+ }
2858
+ i++;
2859
+ out += `"${body}"`;
2860
+ prev = '"';
2861
+ continue;
2862
+ }
2863
+ if (c === ",") {
2864
+ let j = i + 1;
2865
+ while (j < src.length && /\s/.test(src[j])) {
2866
+ j++;
2867
+ }
2868
+ if (src[j] === "}" || src[j] === "]") {
2869
+ i++;
2870
+ continue;
2871
+ }
2872
+ }
2873
+ if ((prev === "{" || prev === ",") && /[A-Za-z_$]/.test(c)) {
2874
+ const m = /^([A-Za-z_$][\w$-]*)(\s*):/.exec(src.slice(i));
2875
+ if (m) {
2876
+ out += `"${m[1]}"${m[2]}:`;
2877
+ i += m[0].length;
2878
+ prev = ":";
2879
+ continue;
2880
+ }
2881
+ }
2882
+ out += c;
2883
+ if (!/\s/.test(c)) {
2884
+ prev = c;
2885
+ }
2886
+ i++;
2887
+ }
2888
+ return out;
2889
+ }
2890
+ function tryParse(text) {
2891
+ try {
2892
+ return { ok: true, value: JSON.parse(text) };
2893
+ } catch (err) {
2894
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
2895
+ }
2896
+ }
2897
+ var MAX_STARTS = 24;
2898
+ function parseLenientJson(raw) {
2899
+ const text = stripReasoning(typeof raw === "string" ? raw : "");
2900
+ if (!text) {
2901
+ return { ok: false, error: "The reply was empty." };
2902
+ }
2903
+ let firstError = "";
2904
+ let sawStart = false;
2905
+ let truncated = false;
2906
+ for (const source of [...fencedBodies(text), text]) {
2907
+ let from = 0;
2908
+ let attempts = 0;
2909
+ while (attempts < MAX_STARTS) {
2910
+ const rel = source.slice(from).search(/[{[]/);
2911
+ if (rel < 0) {
2912
+ break;
2913
+ }
2914
+ const start = from + rel;
2915
+ sawStart = true;
2916
+ attempts++;
2917
+ const span = balancedSpan(source, start);
2918
+ if (!span) {
2919
+ truncated = true;
2920
+ break;
2921
+ }
2922
+ const slice = source.slice(start, span.end);
2923
+ const direct = tryParse(slice);
2924
+ if (direct.ok) {
2925
+ return direct;
2926
+ }
2927
+ const fixed = tryParse(repairJson(slice));
2928
+ if (fixed.ok) {
2929
+ return fixed;
2930
+ }
2931
+ if (!firstError && !direct.ok) {
2932
+ firstError = direct.error;
2933
+ }
2934
+ from = span.end;
2935
+ }
2936
+ }
2937
+ if (!sawStart) {
2938
+ return { ok: false, error: "No JSON object found in the reply." };
2939
+ }
2940
+ if (firstError) {
2941
+ return { ok: false, error: `Invalid JSON: ${firstError}` };
2942
+ }
2943
+ return { ok: false, error: truncated ? "The JSON was cut off before it closed." : "No JSON object found in the reply." };
2944
+ }
2945
+ var REPAIR_ECHO_CHARS = 4e3;
2946
+ function jsonRepairMessages(previousRaw, error) {
2947
+ const previous = stripReasoning(previousRaw).slice(0, REPAIR_ECHO_CHARS);
2948
+ return [
2949
+ { role: "assistant", content: previous || "(empty reply)" },
2950
+ { role: "user", content: `Your previous reply was not valid JSON (${error}). Reply with the JSON object only.` }
2951
+ ];
2952
+ }
2953
+ function JSON_SHAPE_HINT(example) {
2954
+ const shape = typeof example === "string" ? example : JSON.stringify(example);
2955
+ return `Reply with a JSON object only, in exactly this shape and with no other text, no code fence and no comments: ${shape}`;
2956
+ }
2957
+
2958
+ // src/ai/documentBudget.ts
2959
+ var CHARS_PER_TOKEN = 3.5;
2960
+ var FENCE_OPEN2 = /^ {0,3}(`{3,}|~{3,})/;
2961
+ var HEADING2 = /^ {0,3}(#{1,6})[ \t]+(.*?)[ \t]*$/;
2962
+ var FRONTMATTER = /^---[ \t]*\r?\n[\s\S]*?\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/;
2963
+ function frontmatterEnd(markdown) {
2964
+ const m = FRONTMATTER.exec(markdown);
2965
+ return m ? m[0].length : 0;
2966
+ }
2967
+ function splitLines(markdown, from) {
2968
+ const lines = [];
2969
+ let pos = from;
2970
+ while (pos < markdown.length) {
2971
+ const nl = markdown.indexOf("\n", pos);
2972
+ const end = nl < 0 ? markdown.length : nl;
2973
+ lines.push({ text: markdown.slice(pos, end).replace(/\r$/, ""), start: pos });
2974
+ pos = nl < 0 ? markdown.length : nl + 1;
2975
+ }
2976
+ return lines;
2977
+ }
2978
+ function fenceMask(lines) {
2979
+ const mask = [];
2980
+ let fence4 = null;
2981
+ for (const line of lines) {
2982
+ if (!fence4) {
2983
+ const m = FENCE_OPEN2.exec(line.text);
2984
+ if (m) {
2985
+ fence4 = { char: m[1][0], len: m[1].length };
2986
+ mask.push(true);
2987
+ } else {
2988
+ mask.push(false);
2989
+ }
2990
+ continue;
2991
+ }
2992
+ mask.push(true);
2993
+ const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line.text);
2994
+ if (close && close[1][0] === fence4.char && close[1].length >= fence4.len) {
2995
+ fence4 = null;
2996
+ }
2997
+ }
2998
+ return mask;
2999
+ }
3000
+ function headingsOutline(markdown) {
3001
+ const lines = splitLines(markdown, frontmatterEnd(markdown));
3002
+ const mask = fenceMask(lines);
3003
+ const out = [];
3004
+ lines.forEach((line, i) => {
3005
+ if (mask[i]) {
3006
+ return;
3007
+ }
3008
+ const m = HEADING2.exec(line.text);
3009
+ if (!m) {
3010
+ return;
3011
+ }
3012
+ const text = m[2].replace(/[ \t]+#+$/, "").trim();
3013
+ if (text) {
3014
+ out.push({ level: m[1].length, text });
3015
+ }
3016
+ });
3017
+ return out;
3018
+ }
3019
+ function sectionStarts(markdown, lines, mask) {
3020
+ const starts = [0];
3021
+ const bodyStart = lines.length > 0 ? lines[0].start : markdown.length;
3022
+ lines.forEach((line, i) => {
3023
+ if (mask[i] || !HEADING2.test(line.text)) {
3024
+ return;
3025
+ }
3026
+ if (markdown.slice(bodyStart, line.start).trim() === "") {
3027
+ return;
3028
+ }
3029
+ starts.push(line.start);
3030
+ });
3031
+ return starts;
3032
+ }
3033
+ function blockStarts(lines, mask) {
3034
+ const starts = [];
3035
+ for (let i = 1; i < lines.length; i++) {
3036
+ if (lines[i].text.trim() === "") {
3037
+ continue;
3038
+ }
3039
+ if (lines[i - 1].text.trim() === "" && !mask[i - 1]) {
3040
+ starts.push(lines[i].start);
3041
+ }
3042
+ }
3043
+ return starts;
3044
+ }
3045
+ function budgetDocument(markdown, opts) {
3046
+ const total = markdown.length;
3047
+ const budgetTokens = opts.contextTokens - opts.outputTokens - opts.reserveTokens;
3048
+ const budgetChars = Math.max(0, Math.floor(budgetTokens * CHARS_PER_TOKEN));
3049
+ const fmEnd = frontmatterEnd(markdown);
3050
+ const lines = splitLines(markdown, fmEnd);
3051
+ const mask = fenceMask(lines);
3052
+ const starts = sectionStarts(markdown, lines, mask);
3053
+ const sectionsTotal = starts.length;
3054
+ if (total <= budgetChars || total === 0) {
3055
+ return { text: markdown, coverage: 1, truncated: false, sectionsIncluded: sectionsTotal, sectionsTotal };
3056
+ }
3057
+ let cut = 0;
3058
+ let sectionsIncluded = 0;
3059
+ for (let i = 1; i < starts.length; i++) {
3060
+ if (starts[i] > budgetChars) {
3061
+ break;
3062
+ }
3063
+ cut = starts[i];
3064
+ sectionsIncluded = i;
3065
+ }
3066
+ if (cut === 0) {
3067
+ const firstEnd = starts.length > 1 ? starts[1] : total;
3068
+ const blocks = blockStarts(lines, mask).filter((s) => s < firstEnd);
3069
+ cut = blocks.length > 0 ? blocks[0] : firstEnd;
3070
+ for (const s of blocks) {
3071
+ if (s > budgetChars) {
3072
+ break;
3073
+ }
3074
+ cut = Math.max(cut, s);
3075
+ }
3076
+ sectionsIncluded = 1;
3077
+ }
3078
+ if (cut >= total) {
3079
+ return { text: markdown, coverage: 1, truncated: false, sectionsIncluded: sectionsTotal, sectionsTotal };
3080
+ }
3081
+ const text = markdown.slice(0, cut).trimEnd();
3082
+ return {
3083
+ text,
3084
+ coverage: Math.min(0.999, Math.max(1e-3, cut / total)),
3085
+ truncated: true,
3086
+ sectionsIncluded,
3087
+ sectionsTotal
3088
+ };
3089
+ }
3090
+
3091
+ // src/ai/prompts/summary.ts
3092
+ var SUMMARY_SHORT_DOCUMENT_CHARS = 1200;
3093
+ var COMMON_RULES = [
3094
+ "Reply with Markdown only. No preamble, no closing remarks, no code fence around the answer and no title line.",
3095
+ "Use only what the document says. Do not invent facts, names or numbers.",
3096
+ "Write in the language of the document."
3097
+ ];
3098
+ var SUMMARY_SYSTEM_PROMPT = [
3099
+ "You are a writing assistant inside a Markdown editor. You write a short TL;DR of the document the user gives you.",
3100
+ "",
3101
+ "Rules:",
3102
+ '- Write 3 to 6 bullet points, one line each, most important first. Each bullet is a list item starting with "- ".',
3103
+ '- Plain wording that someone who has not read the document can follow. No heading, no "Summary" label.',
3104
+ ...COMMON_RULES.map((r) => `- ${r}`)
3105
+ ].join("\n");
3106
+ var OUTLINE_SYSTEM_PROMPT = [
3107
+ "You are a writing assistant inside a Markdown editor. You write an outline of the document the user gives you.",
3108
+ "",
3109
+ "Rules:",
3110
+ '- Reply with a nested Markdown list: top-level items are the main sections, indented items (two spaces per level, "- " markers) are their subsections or key points. At most three levels.',
3111
+ "- If the document has headings, follow them in order and keep their wording. If it has none, propose short headings that fit its parts.",
3112
+ "- Each item is a few words, not a sentence. No heading syntax (#) and no numbering.",
3113
+ ...COMMON_RULES.map((r) => `- ${r}`)
3114
+ ].join("\n");
3115
+ function fence(text, tag) {
3116
+ return text.split(`</${tag}>`).join(`<\\/${tag}>`);
3117
+ }
3118
+ function coveragePercent(coverage) {
3119
+ return Math.max(1, Math.min(99, Math.round(coverage * 100)));
3120
+ }
3121
+ function buildSummaryMessages(input) {
3122
+ const parts = [];
3123
+ if (input.kind === "tldr") {
3124
+ parts.push(input.document.length < SUMMARY_SHORT_DOCUMENT_CHARS ? "This document is short: write one paragraph of two to four sentences instead of bullet points." : "Write the TL;DR as 3 to 6 bullet points.");
3125
+ } else {
3126
+ parts.push("Write the outline.");
3127
+ }
3128
+ if (input.coverage < 1) {
3129
+ parts.push(`Only the first ${coveragePercent(input.coverage)}% of the document is included below; it was cut to fit. Cover only what is shown and do not guess at the rest.`);
3130
+ }
3131
+ parts.push(`Document:
3132
+ <document>
3133
+ ${fence(input.document, "document")}
3134
+ </document>`);
3135
+ parts.push(input.kind === "tldr" ? "Reply with the TL;DR only." : "Reply with the outline list only.");
3136
+ return [
3137
+ { role: "system", content: input.kind === "tldr" ? SUMMARY_SYSTEM_PROMPT : OUTLINE_SYSTEM_PROMPT },
3138
+ { role: "user", content: parts.join("\n\n") }
3139
+ ];
3140
+ }
3141
+ function summaryMaxTokens(kind, inputChars) {
3142
+ const approxTokens = Math.ceil(inputChars / 3.5);
3143
+ return kind === "tldr" ? Math.max(256, Math.min(700, Math.ceil(approxTokens * 0.2) + 128)) : Math.max(384, Math.min(1200, Math.ceil(approxTokens * 0.3) + 160));
3144
+ }
3145
+ function summaryVisibleText(raw) {
3146
+ return rewriteVisibleText(raw);
3147
+ }
3148
+ function tidy(raw, kind) {
3149
+ let text = rewriteVisibleText(raw).trim();
3150
+ let lines = text.split("\n");
3151
+ const dropLeadIn = () => {
3152
+ if (lines.length > 1 && /^(here('s| is| are)|sure|certainly|of course)\b[^\n]*:\s*$/i.test(lines[0].trim())) {
3153
+ lines.shift();
3154
+ while (lines.length > 0 && lines[0].trim() === "") {
3155
+ lines.shift();
3156
+ }
3157
+ }
3158
+ };
3159
+ dropLeadIn();
3160
+ text = lines.join("\n").trim();
3161
+ const fenced = /^```(?:markdown|md|text)?[ \t]*\n([\s\S]*?)\n```$/i.exec(text);
3162
+ if (fenced) {
3163
+ text = fenced[1].trim();
3164
+ }
3165
+ lines = text.split("\n");
3166
+ dropLeadIn();
3167
+ if (kind === "tldr" && lines.length > 1 && /^(?:#{1,6}\s*)?(?:\*\*|__)?\s*(?:summary|tl;?dr)\s*:?\s*(?:\*\*|__)?\s*:?\s*$/i.test(lines[0].trim())) {
3168
+ lines.shift();
3169
+ }
3170
+ return lines.join("\n").trim();
3171
+ }
3172
+ function dropCutFragment(text) {
3173
+ let t = text.trimEnd();
3174
+ if (!t) {
3175
+ return "";
3176
+ }
3177
+ if (!/[.!?;:)\]"'”’`*_]$/.test(t)) {
3178
+ t = t.replace(/\S+$/, "").trimEnd();
3179
+ }
3180
+ t = t.replace(/(^|\n)[ \t]*(?:[-*+]|\d+[.)]|>|#{1,6})?[ \t]*$/, "").trimEnd();
3181
+ if (!t) {
3182
+ return "";
3183
+ }
3184
+ return /[\p{L}\p{N}]$/u.test(t) ? `${t}\u2026` : `${t} \u2026`;
3185
+ }
3186
+ function summaryPreviewText(raw, kind, cutOff = false) {
3187
+ return cutOff ? dropCutFragment(tidy(raw, kind)) : tidy(raw, kind);
3188
+ }
3189
+ function finalizeSummary(raw, kind, coverage = 1, cutOff = false) {
3190
+ const body = cutOff ? dropCutFragment(tidy(raw, kind)) : tidy(raw, kind);
3191
+ if (!body) {
3192
+ return "";
3193
+ }
3194
+ const note = coverage < 1 ? `Covers the first ${coveragePercent(coverage)}% of the document.` : "";
3195
+ if (kind === "outline") {
3196
+ return note ? `${body}
3197
+
3198
+ ${note}` : body;
3199
+ }
3200
+ const inner = ["**Summary**", "", body, ...note ? ["", note] : []].join("\n");
3201
+ return ["[!NOTE]", inner].join("\n").split("\n").map((l) => l ? `> ${l}` : ">").join("\n");
3202
+ }
3203
+
3204
+ // src/ai/prompts/title.ts
3205
+ var TITLE_MAX_CHARS = 120;
3206
+ var TITLE_OUTLINE_MAX_ITEMS = 60;
3207
+ var TITLE_SYSTEM_PROMPT = [
3208
+ "You are a writing assistant inside a Markdown editor. You suggest titles for the document the user gives you.",
3209
+ "",
3210
+ "Rules:",
3211
+ "- Each title is specific to this document, in plain wording, in Title Case or sentence case to match the document, under 80 characters, with no trailing period and no quotation marks.",
3212
+ "- Make the titles different from each other in angle (descriptive, benefit-led, short).",
3213
+ "- Write in the language of the document.",
3214
+ "- Reply with the JSON object only."
3215
+ ].join("\n");
3216
+ function fence2(text, tag) {
3217
+ return text.split(`</${tag}>`).join(`<\\/${tag}>`);
3218
+ }
3219
+ function renderHeadingOutline(outline, max = TITLE_OUTLINE_MAX_ITEMS) {
3220
+ const shown = outline.slice(0, max).map((h) => `${" ".repeat(Math.max(0, h.level - 1))}- ${h.text}`);
3221
+ if (outline.length > max) {
3222
+ shown.push(`(${outline.length - max} more headings)`);
3223
+ }
3224
+ return shown.join("\n");
3225
+ }
3226
+ function buildTitleMessages(input) {
3227
+ const count = Math.max(1, Math.min(8, input.count ?? 3));
3228
+ const placeholders = Array.from({ length: count }, (_, i) => `title ${i + 1}`);
3229
+ const parts = [`Suggest ${count} titles for this document.`];
3230
+ const existing = (input.existingTitle ?? "").trim();
3231
+ if (existing) {
3232
+ parts.push(`The current title is "${existing.replace(/"/g, "'")}". Do not repeat it.`);
3233
+ }
3234
+ if (input.outline.length > 0) {
3235
+ parts.push(`Headings of the whole document:
3236
+ <headings>
3237
+ ${fence2(renderHeadingOutline(input.outline), "headings")}
3238
+ </headings>`);
3239
+ }
3240
+ parts.push(`Document:
3241
+ <document>
3242
+ ${fence2(input.document, "document")}
3243
+ </document>`);
3244
+ parts.push(JSON_SHAPE_HINT({ titles: placeholders }));
3245
+ return [
3246
+ { role: "system", content: TITLE_SYSTEM_PROMPT },
3247
+ { role: "user", content: parts.join("\n\n") }
3248
+ ];
3249
+ }
3250
+ function titleMaxTokens(count = 3) {
3251
+ return 128 + 48 * Math.max(1, count);
3252
+ }
3253
+ function cleanTitle(value) {
3254
+ let t = value.replace(/\s+/g, " ").trim();
3255
+ t = t.replace(/^["“'‘]+|["”'’]+$/g, "").trim();
3256
+ t = t.replace(/^#{1,6}\s+/, "").replace(/[.。]+$/, "").trim();
3257
+ if (t.length > TITLE_MAX_CHARS) {
3258
+ t = t.slice(0, TITLE_MAX_CHARS).trimEnd();
3259
+ }
3260
+ return t;
3261
+ }
3262
+ var sameTitle = (a, b) => a.toLowerCase() === b.toLowerCase();
3263
+ function parseTitles(raw, existingTitle) {
3264
+ const parsed = parseLenientJson(raw);
3265
+ if (!parsed.ok) {
3266
+ return { ok: false, error: parsed.error };
3267
+ }
3268
+ const v = parsed.value;
3269
+ let list;
3270
+ if (Array.isArray(v)) {
3271
+ list = v;
3272
+ } else if (v && typeof v === "object") {
3273
+ const o = v;
3274
+ list = o.titles ?? (typeof o.title === "string" ? [o.title] : void 0);
3275
+ }
3276
+ if (!Array.isArray(list)) {
3277
+ return { ok: false, error: 'The JSON has no "titles" array.' };
3278
+ }
3279
+ const existing = (existingTitle ?? "").trim();
3280
+ const titles = [];
3281
+ for (const item of list) {
3282
+ if (typeof item !== "string") {
3283
+ continue;
3284
+ }
3285
+ const t = cleanTitle(item);
3286
+ if (!t || existing && sameTitle(t, existing) || titles.some((x) => sameTitle(x, t))) {
3287
+ continue;
3288
+ }
3289
+ titles.push(t);
3290
+ }
3291
+ if (titles.length === 0) {
3292
+ return { ok: false, error: 'The "titles" array has no usable title.' };
3293
+ }
3294
+ return { ok: true, titles };
3295
+ }
3296
+
3297
+ // src/ai/prompts/properties.ts
3298
+ var PROPERTY_MAX_TAGS = 8;
3299
+ var PROPERTY_DESCRIPTION_MAX_CHARS = 200;
3300
+ var PROPERTY_TITLE_MAX_CHARS = 120;
3301
+ var PROPERTY_VOCABULARY_MAX = 80;
3302
+ var PROPERTIES_SYSTEM_PROMPT = [
3303
+ "You are a writing assistant inside a Markdown editor. You suggest frontmatter properties for the document the user gives you: a title, a description and tags.",
3304
+ "",
3305
+ "Rules:",
3306
+ "- title: specific to the document, under 80 characters, no trailing period and no quotation marks.",
3307
+ "- description: one or two plain sentences under 160 characters that say what the document is about. No marketing wording.",
3308
+ '- tags: 3 to 6 short topic tags, lowercase, words joined by hyphens, no "#" and no spaces. Nested tags use a slash, like project/active.',
3309
+ "- Prefer tags from the list of existing tags when they fit. Add a new tag only when none of them does.",
3310
+ "- Do not repeat a tag the document already has.",
3311
+ "- Write in the language of the document.",
3312
+ "- Reply with the JSON object only."
3313
+ ].join("\n");
3314
+ function fence3(text, tag) {
3315
+ return text.split(`</${tag}>`).join(`<\\/${tag}>`);
3316
+ }
3317
+ function entryValue(value) {
3318
+ return Array.isArray(value) ? value.join(", ") : value;
3319
+ }
3320
+ function existingTagsOf(existing) {
3321
+ const out = [];
3322
+ for (const entry of existing) {
3323
+ if (!/^tags?$/i.test(entry.key)) {
3324
+ continue;
3325
+ }
3326
+ const parts = Array.isArray(entry.value) ? entry.value : entry.value.split(/[,\s]+/);
3327
+ for (const p of parts) {
3328
+ const t = normalizeTag(p);
3329
+ if (t && !out.includes(t)) {
3330
+ out.push(t);
3331
+ }
3332
+ }
3333
+ }
3334
+ return out;
3335
+ }
3336
+ function buildPropertiesMessages(input) {
3337
+ const parts = ["Suggest a title, a description and tags for this document."];
3338
+ const props = input.existing.filter((e) => entryValue(e.value).trim() !== "");
3339
+ if (props.length > 0) {
3340
+ parts.push(`Properties it already has:
3341
+ <properties>
3342
+ ${fence3(props.map((e) => `${e.key}: ${entryValue(e.value)}`).join("\n"), "properties")}
3343
+ </properties>`);
3344
+ }
3345
+ const vocabulary = input.vocabulary.slice(0, PROPERTY_VOCABULARY_MAX);
3346
+ if (vocabulary.length > 0) {
3347
+ parts.push(`Prefer these existing tags when they fit:
3348
+ <tags>
3349
+ ${fence3(vocabulary.join(", "), "tags")}
3350
+ </tags>`);
3351
+ }
3352
+ if (input.outline.length > 0) {
3353
+ parts.push(`Headings of the whole document:
3354
+ <headings>
3355
+ ${fence3(renderHeadingOutline(input.outline), "headings")}
3356
+ </headings>`);
3357
+ }
3358
+ parts.push(`Document:
3359
+ <document>
3360
+ ${fence3(input.document, "document")}
3361
+ </document>`);
3362
+ parts.push(JSON_SHAPE_HINT({ title: "the title", description: "one or two sentences", tags: ["tag-one", "tag-two", "tag-three"] }));
3363
+ return [
3364
+ { role: "system", content: PROPERTIES_SYSTEM_PROMPT },
3365
+ { role: "user", content: parts.join("\n\n") }
3366
+ ];
3367
+ }
3368
+ function propertiesMaxTokens() {
3369
+ return 384;
3370
+ }
3371
+ function cleanTag(raw) {
3372
+ const t = normalizeTag(raw.trim().replace(/\s+/g, "-").replace(/[^A-Za-z0-9_/#-]/g, ""));
3373
+ if (!t) {
3374
+ return null;
3375
+ }
3376
+ const segments = t.split("/").map((s) => s.replace(/^-+|-+$/g, "")).filter(Boolean);
3377
+ const joined = segments.join("/");
3378
+ return /[a-z]/.test(joined) ? joined : null;
3379
+ }
3380
+ function oneLine(value, max) {
3381
+ if (typeof value !== "string") {
3382
+ return "";
3383
+ }
3384
+ const t = value.replace(/\s+/g, " ").trim().replace(/^["“]+|["”]+$/g, "").trim();
3385
+ return t.length > max ? t.slice(0, max).trimEnd() : t;
3386
+ }
3387
+ function firstScalar(existing, key) {
3388
+ const entry = existing.find((e) => e.key.toLowerCase() === key);
3389
+ return entry ? entryValue(entry.value).trim() : "";
3390
+ }
3391
+ function parsePropertySuggestions(raw, existing, vocabulary) {
3392
+ const parsed = parseLenientJson(raw);
3393
+ if (!parsed.ok) {
3394
+ return { ok: false, error: parsed.error };
3395
+ }
3396
+ const v = parsed.value;
3397
+ if (!v || typeof v !== "object" || Array.isArray(v)) {
3398
+ return { ok: false, error: "The JSON is not an object." };
3399
+ }
3400
+ const o = v;
3401
+ const suggestions = { tags: [] };
3402
+ const title = oneLine(o.title, PROPERTY_TITLE_MAX_CHARS).replace(/[.。]+$/, "");
3403
+ if (title && title.toLowerCase() !== firstScalar(existing, "title").toLowerCase()) {
3404
+ suggestions.title = title;
3405
+ }
3406
+ const description = oneLine(o.description, PROPERTY_DESCRIPTION_MAX_CHARS);
3407
+ if (description && description.toLowerCase() !== firstScalar(existing, "description").toLowerCase()) {
3408
+ suggestions.description = description;
3409
+ }
3410
+ const rawTags = Array.isArray(o.tags) ? o.tags : typeof o.tags === "string" ? o.tags.split(/[,\n]+/) : [];
3411
+ const present = new Set(existingTagsOf(existing));
3412
+ const known = /* @__PURE__ */ new Set();
3413
+ for (const t of vocabulary) {
3414
+ const n = normalizeTag(t);
3415
+ if (n) {
3416
+ known.add(n);
3417
+ }
3418
+ }
3419
+ const seen = /* @__PURE__ */ new Set();
3420
+ for (const item of rawTags) {
3421
+ if (typeof item !== "string") {
3422
+ continue;
3423
+ }
3424
+ const tag = cleanTag(item);
3425
+ if (!tag || present.has(tag) || seen.has(tag)) {
3426
+ continue;
3427
+ }
3428
+ seen.add(tag);
3429
+ suggestions.tags.push({ tag, known: known.has(tag) });
3430
+ if (suggestions.tags.length >= PROPERTY_MAX_TAGS) {
3431
+ break;
3432
+ }
3433
+ }
3434
+ const hadContent = oneLine(o.title, 1) !== "" || oneLine(o.description, 1) !== "" || rawTags.some((t) => typeof t === "string" && cleanTag(t));
3435
+ if (!hadContent) {
3436
+ return { ok: false, error: "The JSON has no usable title, description or tags." };
3437
+ }
3438
+ return { ok: true, suggestions };
3439
+ }
3440
+
3441
+ // src/ai/privacy.ts
3442
+ var AI_CLOUD_PRIVACY = {
3443
+ leaves: "Text you ask about is sent over the internet to the provider and processed on their servers.",
3444
+ terms: "API traffic is covered by the provider's API terms, which differ from its consumer apps. Whether it is retained or used for training is set by those terms and may change; read them before you decide.",
3445
+ local: "A local engine keeps everything on this machine or your network.",
3446
+ keys: "Your API key stays in this app's secure storage on this machine."
3447
+ };
3448
+ var AI_HOST_PRIVACY = {
3449
+ leaves: "Text you ask about goes through VS Code to GitHub\u2019s Copilot service and is processed on GitHub\u2019s servers, under your Copilot plan.",
3450
+ terms: "Whether it is retained or used for training is set by your plan\u2019s terms, which differ between individual and business plans and may change; read them before you decide.",
3451
+ local: AI_CLOUD_PRIVACY.local,
3452
+ permission: "VS Code also asks for its own permission the first time MikeDown uses Copilot."
3453
+ };
3454
+ function aiConsentStepNote(hostAsks) {
3455
+ return hostAsks ? "MikeDown asks before a cloud engine's first request in each workspace or folder." : "Once allowed, this app sends requests to cloud engines without asking again.";
3456
+ }
3457
+ var AI_SENDS_TEXT = {
3458
+ selection: "the selected text and the paragraph before and after it",
3459
+ document: "the whole document, up to what fits the model",
3460
+ documentAndTags: "the document, its frontmatter and the tag names used in this workspace"
3461
+ };
3462
+ var AI_SENDS_FALLBACK = "the text you ask the AI about";
3463
+ function whatIsSent(feature, sends) {
3464
+ const scope = sends ?? (feature ? getAiFeature(feature)?.sends : void 0);
3465
+ return scope ? AI_SENDS_TEXT[scope] : AI_SENDS_FALLBACK;
3466
+ }
3467
+ function aiProviderPolicyForHost(host, presets = AI_ENGINE_PRESETS) {
3468
+ const h = host?.trim().toLowerCase();
3469
+ if (!h) {
3470
+ return void 0;
3471
+ }
3472
+ return presets.find((p) => p.family === "cloud" && p.policy && hostOfUrl(p.baseUrl) === h)?.policy;
3473
+ }
3474
+ function aiProviderPolicy(engine, presets = AI_ENGINE_PRESETS) {
3475
+ if (engine.kind && isHostEngineKind(engine.kind)) {
3476
+ return presets.find((p) => p.family === "host" && p.kind === engine.kind && p.policy)?.policy;
3477
+ }
3478
+ return aiProviderPolicyForHost(hostOfUrl(engine.baseUrl), presets);
3479
+ }
3480
+ var HOST_KIND_SERVICE = { "vscode-lm": "GitHub Copilot" };
3481
+ function aiConsentHost(engine) {
3482
+ const service = engine.kind ? HOST_KIND_SERVICE[engine.kind] : void 0;
3483
+ return service ?? hostOfUrl(engine.baseUrl) ?? engine.baseUrl;
3484
+ }
3485
+ function aiConsentText(input) {
3486
+ const { engineName: name, host, scopeWord, policy } = input;
3487
+ const sends = whatIsSent(input.feature, input.sends);
3488
+ const paragraphs = input.kind && isHostEngineKind(input.kind) ? [
3489
+ `${name === host ? name : `${name} (${host})`} is a cloud service built into VS Code. If you allow it, this request sends ${sends}. It goes through VS Code over the internet and is processed on the service's servers. ${AI_HOST_PRIVACY.permission}`,
3490
+ AI_HOST_PRIVACY.terms
3491
+ ] : [
3492
+ `${name} is a cloud engine at ${host}. If you allow it, this request sends ${sends}. It goes over the internet and is processed on the provider's servers.`,
3493
+ policy?.note ? `${AI_CLOUD_PRIVACY.terms} ${policy.note}` : AI_CLOUD_PRIVACY.terms
3494
+ ];
3495
+ if (policy && input.policyLine) {
3496
+ paragraphs.push(`${policy.label}: ${policy.url}`);
3497
+ }
3498
+ const allow = `Allow for this ${scopeWord}`;
3499
+ paragraphs.push(scopeWord === "session" ? `You can use a local engine instead. ${allow} lasts until you quit MikeDown.` : `You can use a local engine instead. ${allow} is remembered; you can change it in AI settings \u2192 Cloud consents.`);
3500
+ return {
3501
+ title: `Send text to ${name}?`,
3502
+ body: paragraphs.join("\n\n"),
3503
+ allow,
3504
+ allowOnce: "Allow once",
3505
+ deny: "Don't allow",
3506
+ ...policy && !input.policyLine ? { readPolicy: "Read the provider's policy" } : {}
3507
+ };
3508
+ }
3509
+ var AI_NO_CONSENTS_TEXT = "No cloud engine has been allowed here.";
3510
+ var CONSENT_ANSWER_TEXT = {
3511
+ allow: "Allowed",
3512
+ never: "Not allowed",
3513
+ deny: "Not allowed until restart"
3514
+ };
3515
+ function aiConsentSummary(record, engineName) {
3516
+ const where = record.scope === "folder" ? "this folder" : "this workspace";
3517
+ return `${engineName} \xB7 ${where} \xB7 ${CONSENT_ANSWER_TEXT[record.answer] ?? "Not allowed"}`;
3518
+ }
3519
+
3520
+ // src/ai/setup.ts
3521
+ var GIB = 1024 ** 3;
3522
+ var NOMINAL_SLACK = 0.94;
3523
+ var AI_LOCAL_MIN_UNIFIED_GB = 16;
3524
+ var AI_LOCAL_MIN_MEMORY_GB = 32;
3525
+ var AI_LOCAL_MIN_VRAM_GB = 8;
3526
+ function formatMemoryGb(bytes) {
3527
+ return `${Math.max(1, Math.round(bytes / GIB))} GB`;
3528
+ }
3529
+ function atLeastGb(bytes, gb) {
3530
+ return typeof bytes === "number" && bytes >= gb * GIB * NOMINAL_SLACK;
3531
+ }
3532
+ function nvidiaVram(hw) {
3533
+ return hw.gpu?.vendor === "nvidia" ? hw.gpu.vramBytes : void 0;
3534
+ }
3535
+ function isLocalCapable(hw) {
3536
+ if (!hw) {
3537
+ return false;
3538
+ }
3539
+ if (hw.appleSilicon && atLeastGb(hw.totalMemoryBytes, AI_LOCAL_MIN_UNIFIED_GB)) {
3540
+ return true;
3541
+ }
3542
+ if (atLeastGb(hw.totalMemoryBytes, AI_LOCAL_MIN_MEMORY_GB)) {
3543
+ return true;
3544
+ }
3545
+ return atLeastGb(nvidiaVram(hw), AI_LOCAL_MIN_VRAM_GB);
3546
+ }
3547
+ function modelMemoryBytes(hw) {
3548
+ if (hw.appleSilicon) {
3549
+ return hw.totalMemoryBytes;
3550
+ }
3551
+ const vram = nvidiaVram(hw);
3552
+ return typeof vram === "number" && vram > 0 ? vram : hw.totalMemoryBytes;
3553
+ }
3554
+ function memoryBand(bytes) {
3555
+ if (!atLeastGb(bytes, 24)) {
3556
+ return "small";
3557
+ }
3558
+ return atLeastGb(bytes, 48) ? "large" : "medium";
3559
+ }
3560
+ var SIZE_SENTENCES = {
3561
+ small: "Models up to about 8B parameters run well.",
3562
+ medium: "Models up to about 30B parameters run well.",
3563
+ large: "Larger models work, well beyond 30B parameters."
3564
+ };
3565
+ function localSizeHint(hw) {
3566
+ return hw ? SIZE_SENTENCES[memoryBand(modelMemoryBytes(hw))] : "";
3567
+ }
3568
+ function describeHardware(hw) {
3569
+ if (hw.appleSilicon) {
3570
+ return `Apple Silicon, ${formatMemoryGb(hw.totalMemoryBytes)}`;
3571
+ }
3572
+ const parts = [`${formatMemoryGb(hw.totalMemoryBytes)} of memory`];
3573
+ if (hw.gpu && hw.gpu.vendor !== "apple") {
3574
+ const name = hw.gpu.name?.trim() || (hw.gpu.vendor === "nvidia" ? "NVIDIA GPU" : hw.gpu.vendor === "amd" ? "AMD GPU" : "GPU");
3575
+ parts.push(hw.gpu.vramBytes ? `${name} (${formatMemoryGb(hw.gpu.vramBytes)})` : name);
3576
+ }
3577
+ return parts.join(", ");
3578
+ }
3579
+ var AI_LOCAL_SMALL_MACHINE = "This machine is on the small side for local models; cloud is the easier path.";
3580
+ var AI_LOCAL_ROUGH_GUESS = "A rough guess from memory alone; the graphics card was not checked.";
3581
+ function hardwareLine(hw, capable) {
3582
+ if (!hw) {
3583
+ return "";
3584
+ }
3585
+ if (!capable) {
3586
+ return AI_LOCAL_SMALL_MACHINE;
3587
+ }
3588
+ const rough = !hw.appleSilicon && !hw.gpu ? ` ${AI_LOCAL_ROUGH_GUESS}` : "";
3589
+ return `Local is an option on this machine: ${describeHardware(hw)}. ${localSizeHint(hw)}${rough}`;
3590
+ }
3591
+ function isUsableServer(p) {
3592
+ return p.ok && p.models !== 0;
3593
+ }
3594
+ function presetName(id) {
3595
+ return AI_ENGINE_PRESETS.find((p) => p.id === id)?.name ?? id;
3596
+ }
3597
+ function modelsText(n) {
3598
+ return n === void 0 ? "" : ` with ${n} ${n === 1 ? "model" : "models"}`;
3599
+ }
3600
+ function recommendSetupPath(input) {
3601
+ const hw = input.hardware;
3602
+ const localCapable = isLocalCapable(hw);
3603
+ const base = { localCapable, localHint: localSizeHint(hw), hardwareLine: hardwareLine(hw, localCapable) };
3604
+ const reasons = [];
3605
+ const servers = input.running.map((probe, index) => ({ probe, index })).filter(({ probe }) => isUsableServer(probe)).sort((a, b) => (b.probe.models ?? 0) - (a.probe.models ?? 0) || a.index - b.index);
3606
+ if (servers.length > 0) {
3607
+ const first = servers[0].probe;
3608
+ reasons.push(`${presetName(first.presetId)} is running on this machine${modelsText(first.models)}.`);
3609
+ return { path: "runningLocal", ...base, running: first, reasons };
3610
+ }
3611
+ const idle = input.running.filter((p) => p.ok && p.models === 0);
3612
+ reasons.push(idle.length > 0 ? `${idle.map((p) => presetName(p.presetId)).join(" and ")} answered but lists no models.` : "No local model server is running.");
3613
+ if (input.vscodeLm?.available && input.vscodeLm.models > 0) {
3614
+ reasons.push(`GitHub Copilot offers ${input.vscodeLm.models} ${input.vscodeLm.models === 1 ? "model" : "models"} in VS Code.`);
3615
+ return { path: "vscodeLm", ...base, reasons };
3616
+ }
3617
+ if (input.vscodeLm) {
3618
+ reasons.push("GitHub Copilot offers no model in VS Code.");
3619
+ }
3620
+ if (!hw) {
3621
+ reasons.push("The host did not report this machine\u2019s hardware.");
3622
+ } else {
3623
+ reasons.push(localCapable ? `${describeHardware(hw)} can run local models.` : `${describeHardware(hw)} is below the threshold for local models.`);
3624
+ }
3625
+ return { path: "cloud", ...base, reasons };
3626
+ }
3627
+ function vscodeLmAvailability(probes) {
3628
+ const probe = probes?.find((p) => p.kind === "vscode-lm");
3629
+ return { available: !!probe?.ok && probe.models > 0, models: probe?.ok ? probe.models : 0 };
3630
+ }
3631
+ function modelParamsBillions(id) {
3632
+ const s = id.toLowerCase();
3633
+ const moe = /(?:^|[^a-z0-9.])(\d+)x(\d+(?:\.\d+)?)b(?![a-z0-9])/.exec(s);
3634
+ if (moe) {
3635
+ return Number(moe[1]) * Number(moe[2]);
3636
+ }
3637
+ const m = /(?:^|[^a-z0-9.])(\d+(?:\.\d+)?)([bm])(?![a-z0-9])/.exec(s);
3638
+ if (!m) {
3639
+ return null;
3640
+ }
3641
+ const n = Number(m[1]);
3642
+ if (!Number.isFinite(n) || n <= 0) {
3643
+ return null;
3644
+ }
3645
+ return m[2] === "m" ? n / 1e3 : n;
3646
+ }
3647
+ var TIGHT_FACTOR = 1.75;
3648
+ function fitsBillions(hw) {
3649
+ const memoryBytes = modelMemoryBytes(hw);
3650
+ const gb = memoryBytes / GIB;
3651
+ switch (memoryBand(memoryBytes)) {
3652
+ case "small":
3653
+ return isLocalCapable(hw) ? 8 : Math.min(8, gb / 2);
3654
+ case "medium":
3655
+ return 30;
3656
+ case "large":
3657
+ return gb * 1.1;
3658
+ }
3659
+ }
3660
+ function modelSizeHint(id, hardware, sizeBytes) {
3661
+ if (!hardware) {
3662
+ return void 0;
3663
+ }
3664
+ if (typeof sizeBytes === "number" && sizeBytes > 0) {
3665
+ return modelSizeHintFromBytes(sizeBytes, hardware);
3666
+ }
3667
+ const params = modelParamsBillions(id);
3668
+ if (params === null) {
3669
+ return void 0;
3670
+ }
3671
+ const fits = fitsBillions(hardware);
3672
+ if (params <= fits) {
3673
+ return "fits";
3674
+ }
3675
+ return params <= fits * TIGHT_FACTOR ? "tight" : "too large";
3676
+ }
3677
+ var FITS_SHARE = 0.6;
3678
+ var TIGHT_SHARE = 0.8;
3679
+ function modelSizeHintFromBytes(sizeBytes, hardware) {
3680
+ const memory = modelMemoryBytes(hardware);
3681
+ if (sizeBytes <= memory * FITS_SHARE) {
3682
+ return "fits";
3683
+ }
3684
+ return sizeBytes <= memory * TIGHT_SHARE ? "tight" : "too large";
3685
+ }
3686
+ function formatModelSize(bytes) {
3687
+ const gb = bytes / GIB;
3688
+ return `${gb >= 10 ? Math.round(gb) : Math.round(gb * 10) / 10} GB`;
3689
+ }
3690
+ function modelActiveParamsBillions(id) {
3691
+ const m = /(?:^|[^a-z0-9.])a(\d+(?:\.\d+)?)b(?![a-z0-9])/.exec(id.toLowerCase());
3692
+ const n = m ? Number(m[1]) : NaN;
3693
+ return Number.isFinite(n) && n > 0 ? n : null;
3694
+ }
3695
+ function modelMoeHint(id, moe) {
3696
+ const active = modelActiveParamsBillions(id);
3697
+ return active !== null || moe ? { activeBillions: active } : null;
3698
+ }
3699
+ function modelMoeFastHint(id, moe, size) {
3700
+ if (size !== "fits") {
3701
+ return null;
3702
+ }
3703
+ return modelMoeHint(id, moe);
3704
+ }
3705
+ function localDefaultModel(models) {
3706
+ return models.find((m) => m.chat && !m.missing && m.loaded)?.id ?? null;
3707
+ }
3708
+
3709
+ export {
3710
+ AI_ENGINE_KINDS,
3711
+ AI_HOST_ENGINE_KINDS,
3712
+ AI_CAPABILITY_FLAGS,
3713
+ DEFAULT_CONTEXT_TOKENS,
3714
+ AI_FALLBACK_MODES,
3715
+ AI_ERROR_CODES,
3716
+ SUMMARIZE_MIN_CONTEXT_TOKENS,
3717
+ AI_TASKS,
3718
+ getAiTask,
3719
+ combineRequirements,
3720
+ missingCapabilities,
3721
+ AI_FEATURES,
3722
+ getAiFeature,
3723
+ resolveAiFeature,
3724
+ ANTHROPIC_MODEL_TABLE,
3725
+ OPENAI_MODEL_TABLE,
3726
+ isOpenAiReasoningModel,
3727
+ openAiOffReasoningEffort,
3728
+ detectCapabilities,
3729
+ completeCapabilities,
3730
+ mergeDiscoveredModels,
3731
+ AI_PRESET_FAMILIES,
3732
+ AI_HOST_PRESET_FAMILY,
3733
+ AI_ENGINE_PRESETS,
3734
+ presetForEngine,
3735
+ AI_LOCAL_DETECT_TIMEOUT_MS,
3736
+ localDetectCandidates,
3737
+ parseOpenAiModelList,
3738
+ parseLmStudioModels,
3739
+ parseOmlxModelStatus,
3740
+ withModelStatus,
3741
+ parseModelSizes,
3742
+ withModelSizes,
3743
+ parseOllamaTags,
3744
+ parseOllamaShow,
3745
+ parseOllamaRunning,
3746
+ parseAnthropicModels,
3747
+ anthropicNextPage,
3748
+ localDetectUrl,
3749
+ countDetectedModels,
3750
+ localDetectTarget,
3751
+ AI_SETTINGS_KEYS,
3752
+ isKnownEngineKind,
3753
+ isHostEngineKind,
3754
+ generateAiId,
3755
+ parseAiSettings,
3756
+ serializeAiSettings,
3757
+ isRouteUsable,
3758
+ resolveRoute,
3759
+ SseParser,
3760
+ createOpenAiStreamReader,
3761
+ createAnthropicStreamReader,
3762
+ createCompletionStreamReader,
3763
+ AI_CHARS_PER_TOKEN,
3764
+ aiResultStats,
3765
+ formatAiSeconds,
3766
+ formatAiTokensPerSecond,
3767
+ aiResultStatsText,
3768
+ aiResultStatsTitle,
3769
+ aiTimingLogParts,
3770
+ ANTHROPIC_API_VERSION,
3771
+ ANTHROPIC_DEFAULT_MAX_TOKENS,
3772
+ JSON_ONLY_INSTRUCTION,
3773
+ engineEndpoints,
3774
+ buildAuthHeaders,
3775
+ mergeRequestOverrides,
3776
+ bytesToBase64,
3777
+ base64ToBytes,
3778
+ buildCompletionBody,
3779
+ STREAM_USAGE_HOSTS,
3780
+ streamUsageSupported,
3781
+ withStreamUsage,
3782
+ RETRYABLE_REQUEST_PARAMS,
3783
+ unsupportedRequestParam,
3784
+ withoutRequestParam,
3785
+ buildEmbeddingsBody,
3786
+ parseEmbeddingsResponse,
3787
+ toWireCompletionRequest,
3788
+ fromWireCompletionRequest,
3789
+ AI_ERROR_COPY,
3790
+ aiLengthLimitCopy,
3791
+ isAiErrorCode,
3792
+ isAiError,
3793
+ makeAiError,
3794
+ toAiError,
3795
+ aiErrorCopy,
3796
+ MODEL_LOAD_FAILED_PHRASES,
3797
+ aiErrorCodeFromHttp,
3798
+ thinkingChoices,
3799
+ nearestThinkingLevel,
3800
+ taskThinkingDefault,
3801
+ resolveThinkingLevel,
3802
+ thinkingOverrides,
3803
+ thinkingLevelFrom,
3804
+ withThinkingLevel,
3805
+ THINKING_HEADROOM_TOKENS,
3806
+ THINKING_DEFAULT_HEADROOM_TOKENS,
3807
+ THINKING_MAX_TOKENS_CAP,
3808
+ thinkingMaxTokens,
3809
+ AI_AUDIENCES,
3810
+ getAiAudience,
3811
+ AI_REWRITE_VARIANTS,
3812
+ getAiRewriteVariant,
3813
+ REWRITE_CONTEXT_CHARS,
3814
+ REWRITE_INSTRUCTION_MAX_CHARS,
3815
+ REWRITE_LANGUAGE_MAX_CHARS,
3816
+ REWRITE_SYSTEM_PROMPT,
3817
+ TRANSLATE_SYSTEM_ADDENDUM,
3818
+ EXPLAIN_SYSTEM_ADDENDUM,
3819
+ TRANSLATE_DOCUMENT_NOTE,
3820
+ escapeClosingTag,
3821
+ trimRewriteContext,
3822
+ normalizeRewriteLanguage,
3823
+ rewriteInstruction,
3824
+ rewriteSystemPrompt,
3825
+ buildRewriteMessages,
3826
+ EXPLAIN_MAX_TOKENS,
3827
+ rewriteMaxTokens,
3828
+ rewriteVisibleText,
3829
+ finalizeRewriteText,
3830
+ EXPLAIN_SYSTEM_PROMPT,
3831
+ buildExplainMessages,
3832
+ explainMaxTokens,
3833
+ countMarkdownStructure,
3834
+ checkMarkdownStructure,
3835
+ parseLenientJson,
3836
+ jsonRepairMessages,
3837
+ JSON_SHAPE_HINT,
3838
+ CHARS_PER_TOKEN,
3839
+ headingsOutline,
3840
+ budgetDocument,
3841
+ SUMMARY_SHORT_DOCUMENT_CHARS,
3842
+ SUMMARY_SYSTEM_PROMPT,
3843
+ OUTLINE_SYSTEM_PROMPT,
3844
+ coveragePercent,
3845
+ buildSummaryMessages,
3846
+ summaryMaxTokens,
3847
+ summaryVisibleText,
3848
+ summaryPreviewText,
3849
+ finalizeSummary,
3850
+ TITLE_MAX_CHARS,
3851
+ TITLE_OUTLINE_MAX_ITEMS,
3852
+ TITLE_SYSTEM_PROMPT,
3853
+ renderHeadingOutline,
3854
+ buildTitleMessages,
3855
+ titleMaxTokens,
3856
+ parseTitles,
3857
+ PROPERTY_MAX_TAGS,
3858
+ PROPERTY_DESCRIPTION_MAX_CHARS,
3859
+ PROPERTY_TITLE_MAX_CHARS,
3860
+ PROPERTY_VOCABULARY_MAX,
3861
+ PROPERTIES_SYSTEM_PROMPT,
3862
+ existingTagsOf,
3863
+ buildPropertiesMessages,
3864
+ propertiesMaxTokens,
3865
+ parsePropertySuggestions,
3866
+ AI_CLOUD_PRIVACY,
3867
+ AI_HOST_PRIVACY,
3868
+ aiConsentStepNote,
3869
+ AI_SENDS_TEXT,
3870
+ AI_SENDS_FALLBACK,
3871
+ whatIsSent,
3872
+ aiProviderPolicyForHost,
3873
+ aiProviderPolicy,
3874
+ aiConsentHost,
3875
+ aiConsentText,
3876
+ AI_NO_CONSENTS_TEXT,
3877
+ aiConsentSummary,
3878
+ AI_LOCAL_MIN_UNIFIED_GB,
3879
+ AI_LOCAL_MIN_MEMORY_GB,
3880
+ AI_LOCAL_MIN_VRAM_GB,
3881
+ formatMemoryGb,
3882
+ isLocalCapable,
3883
+ modelMemoryBytes,
3884
+ localSizeHint,
3885
+ describeHardware,
3886
+ AI_LOCAL_SMALL_MACHINE,
3887
+ AI_LOCAL_ROUGH_GUESS,
3888
+ recommendSetupPath,
3889
+ vscodeLmAvailability,
3890
+ modelParamsBillions,
3891
+ modelSizeHint,
3892
+ modelSizeHintFromBytes,
3893
+ formatModelSize,
3894
+ modelActiveParamsBillions,
3895
+ modelMoeHint,
3896
+ modelMoeFastHint,
3897
+ localDefaultModel
3898
+ };
3899
+ //# sourceMappingURL=chunk-JZWKEBCI.js.map