@mikedown/core 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +121 -2
- package/dist/ai/engines-page.d.ts +137 -0
- package/dist/ai/engines-page.js +11 -0
- package/dist/ai/engines-page.js.map +1 -0
- package/dist/ai/index.d.ts +2722 -0
- package/dist/ai/index.js +684 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai-ui-QFYFN73D.js +6397 -0
- package/dist/ai-ui-QFYFN73D.js.map +1 -0
- package/dist/chunk-APPBNQEY.js +216 -0
- package/dist/chunk-APPBNQEY.js.map +1 -0
- package/dist/chunk-DWXGWVA3.js +5748 -0
- package/dist/chunk-DWXGWVA3.js.map +1 -0
- package/dist/chunk-GWCRPPCX.js +18 -0
- package/dist/chunk-GWCRPPCX.js.map +1 -0
- package/dist/chunk-IWCXWUWW.js +6958 -0
- package/dist/chunk-IWCXWUWW.js.map +1 -0
- package/dist/chunk-L4FO63DF.js +113 -0
- package/dist/chunk-L4FO63DF.js.map +1 -0
- package/dist/chunk-N4N54LK2.js +39 -0
- package/dist/chunk-N4N54LK2.js.map +1 -0
- package/dist/chunk-QWKT57TR.js +62 -0
- package/dist/chunk-QWKT57TR.js.map +1 -0
- package/dist/chunk-TSP7BAV3.js +22 -0
- package/dist/chunk-TSP7BAV3.js.map +1 -0
- package/dist/index.d.ts +19 -463
- package/dist/index.js +7717 -2103
- package/dist/index.js.map +1 -1
- package/dist/schema-DVfssxQX.d.ts +100 -0
- package/dist/shared/anchoring.js +4 -16
- package/dist/shared/anchoring.js.map +1 -1
- package/dist/shared/imagePaste.js +3 -57
- package/dist/shared/imagePaste.js.map +1 -1
- package/dist/shared/sha1.js +3 -57
- package/dist/shared/sha1.js.map +1 -1
- package/dist/shared/wordCount.js +4 -12
- package/dist/shared/wordCount.js.map +1 -1
- package/dist/styles.css +4562 -108
- package/dist/types-2TZErbJr.d.ts +619 -0
- package/dist/types-DO8UBybZ.d.ts +741 -0
- package/package.json +11 -1
|
@@ -0,0 +1,2722 @@
|
|
|
1
|
+
import { c as AiModelCapabilities, d as AiCapabilityFlag, e as AiSendsScope, f as AiFeatureSettings, g as AiLocality, h as AiEngineConfig, i as AiEngineKind, j as AiModelConfig, k as AiDiscoveredModel, l as AiApiKeyFile, m as AiDocsOptions, A as AiSettings, a as AiEngineStatus, n as AiFinishReason, o as AiUsage, p as AiErrorCode, q as AiCompletionResult, r as AiCompletionRequest, s as AiMessage, t as AiError, u as AiDocsLink, v as AiDocsIndex, w as AiDocsPage, x as AiDocsHome, y as AiDocsSectionRef, z as AiDocsSection, B as AiDocBaseStatus, b as MikeDownDocsHost, C as AiDocsNavigateBy, D as AiConsentRecord, E as AiLocalServerProbe, F as AiHardwareInfo, G as AiHostEngineProbe } from '../types-DO8UBybZ.js';
|
|
2
|
+
export { H as AI_CAPABILITY_FLAGS, I as AI_DOCS_MAX_HOPS_DEFAULT, J as AI_DOCS_MAX_HOPS_MAX, K as AI_DOCS_MAX_HOPS_MIN, L as AI_DOCS_NAVIGATE_BY, N as AI_DOCS_NAVIGATE_BY_DEFAULT, O as AI_DOCS_SOURCE_SHARE_DEFAULT, P as AI_DOCS_SOURCE_SHARE_MAX, Q as AI_DOCS_SOURCE_SHARE_MIN, R as AI_ENGINE_KINDS, S as AI_ERROR_CODES, T as AI_FALLBACK_MODES, U as AI_HOST_ENGINE_KINDS, V as AI_TURN_GRANT_MAX_USES, W as AiCapabilitiesSource, X as AiCompletionTurn, Y as AiConsentAbout, Z as AiConsentReply, _ as AiDocBase, $ as AiDocHomeKind, a0 as AiDocRootStatus, a1 as AiDocsHeading, a2 as AiDocsSettings, a3 as AiEngineStatusModel, a4 as AiFallbackMode, a5 as AiFeatureSetting, a6 as AiGhostTextOptions, a7 as AiGhostTrigger, a8 as AiKeySource, a9 as AiLocalitySetting, aa as AiMessagePart, ab as AiRoute, ac as AiRouting, ad as AiServedBy, ae as AiTiming, af as AiTurnConsentAbout, ag as DEFAULT_CONTEXT_TOKENS, M as MikeDownAiHost, ah as ProviderAdapter } from '../types-DO8UBybZ.js';
|
|
3
|
+
export { a as AI_SETTINGS_KEYS, A as AiSettingsPatch, D as DocRootCheck, G as GHOST_DELAY_MS_DEFAULT, b as GHOST_DELAY_MS_MAX, c as GHOST_DELAY_MS_MIN, d as GHOST_MAX_TOKENS_DEFAULT, e as GHOST_MAX_TOKENS_MAX, f as GHOST_MAX_TOKENS_MIN, P as ParseAiSettingsOptions, g as checkDocRoot, h as docRootKey, i as generateAiId, j as ghostTextOptions, k as isHostEngineKind, l as isKnownEngineKind, n as normalizeDocHomeFile, p as parseAiSettings, m as parseDocBases, o as parseDocsOptions, r as resolveDocsSettings, s as serializeAiSettings } from '../schema-DVfssxQX.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Task taxonomy. A feature names a task; the router picks the engine and
|
|
7
|
+
* model routed to that task and checks the model has what the task needs.
|
|
8
|
+
* Task ids are the keys of `routing.tasks`; unknown ids saved by a newer
|
|
9
|
+
* version are kept and route like any other (with no requirements).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** A task's needs. Booleans must be true on the model; `contextTokens` is a minimum. */
|
|
13
|
+
type AiCapabilityRequirements = Partial<AiModelCapabilities>;
|
|
14
|
+
interface AiTaskDefinition {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
description: string;
|
|
18
|
+
requires: AiCapabilityRequirements;
|
|
19
|
+
/**
|
|
20
|
+
* Nice to have, never a gate: the router tries candidates that meet these
|
|
21
|
+
* first and still uses one that does not (a model without JSON mode, a
|
|
22
|
+
* small context window).
|
|
23
|
+
*/
|
|
24
|
+
prefers?: AiCapabilityRequirements;
|
|
25
|
+
/** At least one of these flags must be true (ghost text: a FIM model, or a chat model with a continue prompt). */
|
|
26
|
+
requiresAny?: AiCapabilityFlag[];
|
|
27
|
+
/** Served by a host method rather than a model completion. */
|
|
28
|
+
hostMethod?: 'embed' | 'transcribe';
|
|
29
|
+
}
|
|
30
|
+
/** The "comfortable" context for `summarize`: preferred by the router, and the point below which the UI may warn. Not a gate. */
|
|
31
|
+
declare const SUMMARIZE_MIN_CONTEXT_TOKENS = 32768;
|
|
32
|
+
declare const AI_TASKS: readonly AiTaskDefinition[];
|
|
33
|
+
declare function getAiTask(id: string): AiTaskDefinition | undefined;
|
|
34
|
+
/** Merge two requirement sets: flags OR together, `contextTokens` takes the larger minimum. */
|
|
35
|
+
declare function combineRequirements(a: AiCapabilityRequirements, b: AiCapabilityRequirements): AiCapabilityRequirements;
|
|
36
|
+
/**
|
|
37
|
+
* The capabilities a model lacks for these requirements, as flag names (and
|
|
38
|
+
* `contextTokens` when the window is too small). Empty means it qualifies.
|
|
39
|
+
* An unknown context window counts as `DEFAULT_CONTEXT_TOKENS`.
|
|
40
|
+
*/
|
|
41
|
+
declare function missingCapabilities(caps: AiModelCapabilities, requires: AiCapabilityRequirements, requiresAny?: readonly AiCapabilityFlag[], defaultContextTokens?: number): Array<keyof AiModelCapabilities>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Feature registry. A feature's defaults (on or off, which task it uses,
|
|
45
|
+
* what else it needs from a model) live here, so adding a feature is one
|
|
46
|
+
* entry plus its UI and never a settings migration. `features.<id>` in
|
|
47
|
+
* settings overrides `enabled` and `task`.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
interface AiFeatureDefinition {
|
|
51
|
+
id: string;
|
|
52
|
+
label: string;
|
|
53
|
+
description: string;
|
|
54
|
+
defaultEnabled: boolean;
|
|
55
|
+
defaultTask: string;
|
|
56
|
+
/** Needs on top of the task's own. */
|
|
57
|
+
requires: AiCapabilityRequirements;
|
|
58
|
+
/** `planned` features are listed greyed ("coming later") and never surface. */
|
|
59
|
+
status: 'available' | 'planned';
|
|
60
|
+
/** What a request sends, for the consent dialogs and tooltips (`whatIsSent`). Absent: a generic sentence. */
|
|
61
|
+
sends?: AiSendsScope;
|
|
62
|
+
}
|
|
63
|
+
declare const AI_FEATURES: readonly AiFeatureDefinition[];
|
|
64
|
+
declare function getAiFeature(id: string): AiFeatureDefinition | undefined;
|
|
65
|
+
interface ResolvedAiFeature {
|
|
66
|
+
id: string;
|
|
67
|
+
/** Saved value, else the registry default. Unknown features default to off. */
|
|
68
|
+
enabled: boolean;
|
|
69
|
+
/** Saved override, else the registry default task, else the feature id. */
|
|
70
|
+
task: string;
|
|
71
|
+
requires: AiCapabilityRequirements;
|
|
72
|
+
definition?: AiFeatureDefinition;
|
|
73
|
+
}
|
|
74
|
+
/** A feature's effective state from saved settings plus the registry. Ignores the master switch. */
|
|
75
|
+
declare function resolveAiFeature(features: AiFeatureSettings, id: string): ResolvedAiFeature;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Local or cloud, from an engine's base URL. Loopback, link-local, IPv6
|
|
79
|
+
* unique-local, `localhost`, `.local`, `.lan`, `.home.arpa` and `.internal`
|
|
80
|
+
* are always local; `localNetworks` adds CIDR ranges (RFC 1918 by default, a
|
|
81
|
+
* WireGuard range such as `10.0.98.0/24`) and `.suffix` host names. Anything
|
|
82
|
+
* else, including a URL that does not parse, is cloud.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/** Default `mikedown.ai.localNetworks`: RFC 1918. */
|
|
86
|
+
declare const DEFAULT_LOCAL_NETWORKS: readonly string[];
|
|
87
|
+
/** Lower-cased host of a URL (no port, no brackets), or null. Accepts a bare `host:port`. */
|
|
88
|
+
declare function hostOfUrl(url: string): string | null;
|
|
89
|
+
/**
|
|
90
|
+
* `local` or `cloud` for a base URL. `localNetworks` entries are IPv4 CIDR
|
|
91
|
+
* ranges (`10.0.98.0/24`, a bare address counts as /32) or host-name
|
|
92
|
+
* suffixes (`.ts.net`, `studio.example.com`).
|
|
93
|
+
*/
|
|
94
|
+
declare function detectLocality(baseUrl: string, localNetworks?: readonly string[]): AiLocality;
|
|
95
|
+
/**
|
|
96
|
+
* The engine's effective locality: its pin, or `detectLocality` for `auto`.
|
|
97
|
+
* A `vscode-lm` engine is always cloud, whatever its pin: it has no address,
|
|
98
|
+
* and the text goes to GitHub's Copilot service.
|
|
99
|
+
*/
|
|
100
|
+
declare function engineLocality(engine: Pick<AiEngineConfig, 'baseUrl' | 'locality'> & {
|
|
101
|
+
kind?: string;
|
|
102
|
+
}, localNetworks?: readonly string[]): AiLocality;
|
|
103
|
+
/**
|
|
104
|
+
* True when a pinned locality disagrees with the URL (cloud pinned on a
|
|
105
|
+
* private address, or local pinned on a public one). The indicator shows a
|
|
106
|
+
* warning glyph for it.
|
|
107
|
+
*/
|
|
108
|
+
declare function localityMismatch(engine: Pick<AiEngineConfig, 'baseUrl' | 'locality'> & {
|
|
109
|
+
kind?: string;
|
|
110
|
+
}, localNetworks?: readonly string[]): boolean;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Capability detection for models the endpoint does not describe (oMLX and
|
|
114
|
+
* most OpenAI-compatible servers list bare ids). Order of trust: endpoint
|
|
115
|
+
* metadata, then these heuristics, then user edits, which always win and
|
|
116
|
+
* survive re-discovery (`capabilitiesSource: 'user'`).
|
|
117
|
+
*
|
|
118
|
+
* Local models: an ordered regex table of families; every matching rule
|
|
119
|
+
* applies in order, so later, more specific rules override earlier ones.
|
|
120
|
+
* Cloud models (Anthropic, OpenAI): static tables matched by the longest id
|
|
121
|
+
* prefix, so a new model in a known line inherits its line's values.
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
type CloudTable = Array<{
|
|
125
|
+
prefix: string;
|
|
126
|
+
caps: Partial<AiModelCapabilities>;
|
|
127
|
+
}>;
|
|
128
|
+
/** Anthropic models, matched by the longest prefix. Unknown `claude-*` ids get the generic row. */
|
|
129
|
+
declare const ANTHROPIC_MODEL_TABLE: CloudTable;
|
|
130
|
+
/** OpenAI models, matched by the longest prefix. */
|
|
131
|
+
declare const OPENAI_MODEL_TABLE: CloudTable;
|
|
132
|
+
/**
|
|
133
|
+
* OpenAI's reasoning models, which take only the default sampling: Chat
|
|
134
|
+
* Completions rejects a `temperature` or `top_p` other than 1 for them. The
|
|
135
|
+
* native `openai` kind matches on the id; any other kind only on an
|
|
136
|
+
* `openai/` id (OpenRouter), so a local model that happens to be called
|
|
137
|
+
* `o3-something` keeps its sampling. `-chat-latest` snapshots match too:
|
|
138
|
+
* the default sampling costs nothing there.
|
|
139
|
+
*/
|
|
140
|
+
declare function isOpenAiReasoningModel(modelId: string, kind?: AiEngineKind): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* The `reasoning_effort` that means "Off" for an OpenAI reasoning model, or
|
|
143
|
+
* null when `modelId` is not one (the caller leaves the request alone). The
|
|
144
|
+
* Thinking dial stores Off as `none`, which only some families accept:
|
|
145
|
+
*
|
|
146
|
+
* - `gpt-5.1` onwards in the 5.x line (5.5 included): `none`.
|
|
147
|
+
* - `gpt-5`, `gpt-5-mini`, `gpt-5-nano`: `minimal`.
|
|
148
|
+
* - everything else (o-series, `gpt-6`+, codex and pro variants, ids not
|
|
149
|
+
* documented yet): `low`, the one value every reasoning family accepts.
|
|
150
|
+
* Newer models reject `none` (and some `minimal`), so unknown means `low`.
|
|
151
|
+
*/
|
|
152
|
+
declare function openAiOffReasoningEffort(modelId: string, kind?: AiEngineKind): 'none' | 'minimal' | 'low' | null;
|
|
153
|
+
interface DetectedCapabilities {
|
|
154
|
+
capabilities: AiModelCapabilities;
|
|
155
|
+
/** The family rule or table prefix that matched last, or null for the generic default. */
|
|
156
|
+
family: string | null;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Capabilities guessed from a model id. A `vscode-lm` model is a plain chat
|
|
160
|
+
* model (the endpoint gives its context window). Anthropic and OpenAI engines use
|
|
161
|
+
* their static tables; any engine whose id (after a `vendor/` prefix, as on
|
|
162
|
+
* OpenRouter) starts with `claude-` or a known OpenAI line does too. An id
|
|
163
|
+
* nothing matches is a plain chat model with `DEFAULT_CONTEXT_TOKENS`.
|
|
164
|
+
*/
|
|
165
|
+
declare function detectCapabilities(modelId: string, kind?: AiEngineKind): DetectedCapabilities;
|
|
166
|
+
/** Endpoint values over heuristics: fields the endpoint left out (or null) come from the id. */
|
|
167
|
+
declare function completeCapabilities(partial: Partial<AiModelCapabilities>, modelId: string, kind?: AiEngineKind): AiModelCapabilities;
|
|
168
|
+
/**
|
|
169
|
+
* Merge one discovery into an engine's saved models, by id:
|
|
170
|
+
*
|
|
171
|
+
* - listed and saved: `endpoint`/`heuristic` rows get the fresh values, `user`
|
|
172
|
+
* rows keep theirs; `enabled`, `label` and `requestOverrides` are kept, and
|
|
173
|
+
* `missing` is cleared;
|
|
174
|
+
* - saved but not listed: marked `missing: true` (greyed, not deleted), except
|
|
175
|
+
* manual entries, which are left alone;
|
|
176
|
+
* - listed but new: appended in discovery order, enabled unless
|
|
177
|
+
* `opts.addEnabled` is false. Cloud engines pass false (OPEN-12): a
|
|
178
|
+
* 300-model list must not flood the routing pickers, so the user picks.
|
|
179
|
+
*/
|
|
180
|
+
declare function mergeDiscoveredModels(existing: readonly AiModelConfig[], discovered: readonly AiDiscoveredModel[], kind?: AiEngineKind, opts?: {
|
|
181
|
+
addEnabled?: boolean;
|
|
182
|
+
}): AiModelConfig[];
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* "Add engine" presets. A preset only fills the new engine's name, kind, base
|
|
186
|
+
* URL, key variable and key file; everything else is the user's. Presets also
|
|
187
|
+
* drive the Express engine form: which family a card sits in, the caption
|
|
188
|
+
* under it, where to get a key, and which fetched models count as Popular.
|
|
189
|
+
*
|
|
190
|
+
* No model ids here: they go stale, and the endpoint is the source of truth.
|
|
191
|
+
* Popular rules are patterns matched against whatever the endpoint lists.
|
|
192
|
+
*/
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Where a preset runs: your own server, a cloud API you paste a key for, or
|
|
196
|
+
* a model API built into the host (`host`: VS Code's GitHub Copilot models,
|
|
197
|
+
* no address and no key; shown only where the host reports it).
|
|
198
|
+
*/
|
|
199
|
+
type AiPresetFamily = 'local' | 'cloud' | 'host';
|
|
200
|
+
/**
|
|
201
|
+
* One Popular slot: the newest fetched id that matches `match` (and not
|
|
202
|
+
* `exclude`). An id `prefer` matches (an alias such as `-latest`) wins;
|
|
203
|
+
* otherwise "newest" compares the version numbers in the id, then a date
|
|
204
|
+
* stamp.
|
|
205
|
+
*/
|
|
206
|
+
interface AiPopularRule {
|
|
207
|
+
/** Short family name, for the picker's tooltip ("Sonnet"). */
|
|
208
|
+
label: string;
|
|
209
|
+
match: RegExp;
|
|
210
|
+
exclude?: RegExp;
|
|
211
|
+
prefer?: RegExp;
|
|
212
|
+
}
|
|
213
|
+
interface AiEnginePreset {
|
|
214
|
+
id: string;
|
|
215
|
+
name: string;
|
|
216
|
+
kind: AiEngineKind;
|
|
217
|
+
baseUrl: string;
|
|
218
|
+
apiKeyEnv?: string;
|
|
219
|
+
apiKeyFile?: AiApiKeyFile;
|
|
220
|
+
/** Whether the endpoint expects a key at all (LM Studio accepts any). */
|
|
221
|
+
needsKey: boolean;
|
|
222
|
+
family: AiPresetFamily;
|
|
223
|
+
/** The quick-start card's one-line caption. */
|
|
224
|
+
caption: string;
|
|
225
|
+
/** Under the API key field: where to get one, or whether one is needed. A link to `keyUrl` when it has one. */
|
|
226
|
+
keyHint: string;
|
|
227
|
+
/** Cloud presets: the provider's page that creates API keys, opened through the host's `openLink`. */
|
|
228
|
+
keyUrl?: string;
|
|
229
|
+
/** Cloud presets: the Popular group, in order. The first rule that matches is the auto-picked default. */
|
|
230
|
+
popular?: readonly AiPopularRule[];
|
|
231
|
+
/**
|
|
232
|
+
* Local presets: the path (from the server root, without `/v1`) that local
|
|
233
|
+
* server detection GETs when it is not `{baseUrl}/models`. The native lists
|
|
234
|
+
* answer without a key.
|
|
235
|
+
*/
|
|
236
|
+
detectPath?: string;
|
|
237
|
+
/** Cloud and host presets: the provider's own page on how API data is used (see `privacy.ts`). */
|
|
238
|
+
policy?: AiProviderPolicy;
|
|
239
|
+
/**
|
|
240
|
+
* Cloud presets: the provider's official API pricing page, linked as
|
|
241
|
+
* "Pricing" beside the key link. Only the link: prices change, so no
|
|
242
|
+
* figure is stated in code. URLs checked 2026-10-29.
|
|
243
|
+
*/
|
|
244
|
+
pricingUrl?: string;
|
|
245
|
+
/**
|
|
246
|
+
* Local presets: the official download or install page, for a server that
|
|
247
|
+
* is not running yet ("Install", then Look again). URLs checked 2026-10-29.
|
|
248
|
+
*/
|
|
249
|
+
installUrl?: string;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* A provider's official privacy or API data-use page. Only the link: what a
|
|
253
|
+
* provider does with the text is set by its terms, which change, so no claim
|
|
254
|
+
* about training or retention is made in code. URLs checked 2026-10-29.
|
|
255
|
+
*/
|
|
256
|
+
interface AiProviderPolicy {
|
|
257
|
+
/** Link text ("Anthropic privacy and data use"). */
|
|
258
|
+
label: string;
|
|
259
|
+
url: string;
|
|
260
|
+
/** One extra neutral sentence, for a provider that passes requests on (OpenRouter). */
|
|
261
|
+
note?: string;
|
|
262
|
+
}
|
|
263
|
+
interface AiPresetFamilyInfo {
|
|
264
|
+
family: AiPresetFamily;
|
|
265
|
+
/** Group heading over the quick-start cards. */
|
|
266
|
+
label: string;
|
|
267
|
+
/** The "two ways to connect" line for this family. */
|
|
268
|
+
explainer: string;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The two quick-start groups, in display order, with the "two ways to
|
|
272
|
+
* connect" copy. The `host` family is not in this list: it shows only on a
|
|
273
|
+
* host whose `detectHostEngines` reports a model (`AI_HOST_PRESET_FAMILY`).
|
|
274
|
+
*/
|
|
275
|
+
declare const AI_PRESET_FAMILIES: readonly AiPresetFamilyInfo[];
|
|
276
|
+
/** The `host` group (VS Code's built-in models), for the hosts that report one through `detectHostEngines`. */
|
|
277
|
+
declare const AI_HOST_PRESET_FAMILY: AiPresetFamilyInfo;
|
|
278
|
+
declare const AI_ENGINE_PRESETS: readonly AiEnginePreset[];
|
|
279
|
+
/**
|
|
280
|
+
* The preset a saved engine most likely came from (engines do not store
|
|
281
|
+
* one): same kind and address, then the same key variable as a tiebreak
|
|
282
|
+
* (oMLX and vLLM share port 8000); else, for a cloud address, the preset on
|
|
283
|
+
* the same host; else the one preset of a dedicated kind. Undefined when
|
|
284
|
+
* nothing fits (a hand-made OpenAI-compatible engine).
|
|
285
|
+
*/
|
|
286
|
+
declare function presetForEngine(engine: {
|
|
287
|
+
kind: string;
|
|
288
|
+
baseUrl: string;
|
|
289
|
+
apiKeyEnv?: string;
|
|
290
|
+
}, presets?: readonly AiEnginePreset[]): AiEnginePreset | undefined;
|
|
291
|
+
/** Local server detection waits this long for each address. */
|
|
292
|
+
declare const AI_LOCAL_DETECT_TIMEOUT_MS = 1500;
|
|
293
|
+
/** What `detectLocalServers` is asked about: every local preset but Custom, at its default address. */
|
|
294
|
+
declare function localDetectCandidates(presets?: readonly AiEnginePreset[]): {
|
|
295
|
+
presetId: string;
|
|
296
|
+
baseUrl: string;
|
|
297
|
+
}[];
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Pure parsers for discovery responses, one per endpoint shape. Hosts do the
|
|
301
|
+
* HTTP and hand the parsed JSON here, so the VS Code adapters and the Rust
|
|
302
|
+
* port read the same fixtures (`test/ai-fixtures/discovery/`). Every parser
|
|
303
|
+
* tolerates junk: entries without a string id are skipped, never thrown on.
|
|
304
|
+
*/
|
|
305
|
+
|
|
306
|
+
/** `GET {base}/models` on any OpenAI-compatible server: ids only, capabilities from heuristics. */
|
|
307
|
+
declare function parseOpenAiModelList(body: unknown): AiDiscoveredModel[];
|
|
308
|
+
/**
|
|
309
|
+
* LM Studio `GET /api/v0/models`: `type` (`llm`, `vlm`, `embeddings`),
|
|
310
|
+
* `max_context_length`, `state` (`loaded` / `not-loaded`), `arch` (a `moe`
|
|
311
|
+
* in it marks a mixture of experts; one ending `_mtp` is a speculative-decoding
|
|
312
|
+
* drafter, not a chat model), and on newer builds `capabilities`
|
|
313
|
+
* (`tool_use`).
|
|
314
|
+
*/
|
|
315
|
+
declare function parseLmStudioModels(body: unknown): AiDiscoveredModel[];
|
|
316
|
+
/**
|
|
317
|
+
* oMLX `GET {base}/models/status` (beside its OpenAI `/models`): per model
|
|
318
|
+
* `loaded` and `model_type` (`llm`, `vlm`, `embedding`, or a non-text type
|
|
319
|
+
* such as `audio_stt` or `markitdown`, which can neither chat nor embed),
|
|
320
|
+
* `model_context_length`, `estimated_size` (bytes) and `config_model_type`
|
|
321
|
+
* (a `moe` in it marks a mixture of experts; one ending `_mtp` is a
|
|
322
|
+
* speculative-decoding drafter, not a chat model, though it says `llm`).
|
|
323
|
+
*/
|
|
324
|
+
declare function parseOmlxModelStatus(body: unknown): AiDiscoveredModel[];
|
|
325
|
+
/**
|
|
326
|
+
* Fold a status listing (`parseOmlxModelStatus`) into the models `/models`
|
|
327
|
+
* listed: the listing decides which models exist, the status adds what it
|
|
328
|
+
* knows about each (capabilities, loaded state).
|
|
329
|
+
*/
|
|
330
|
+
declare function withModelStatus(listed: readonly AiDiscoveredModel[], status: readonly AiDiscoveredModel[]): AiDiscoveredModel[];
|
|
331
|
+
/**
|
|
332
|
+
* Sizes in bytes by model id from a listing that has them: LM Studio
|
|
333
|
+
* `GET /api/v1/models` (`models[].key`, `size_bytes`; its `/api/v0` has
|
|
334
|
+
* none) and Ollama `GET /api/tags` (`models[].model` or `name`, `size`).
|
|
335
|
+
*/
|
|
336
|
+
declare function parseModelSizes(body: unknown): Map<string, number>;
|
|
337
|
+
/** `models` with the sizes `parseModelSizes` found added; the rest as they were. */
|
|
338
|
+
declare function withModelSizes(models: readonly AiDiscoveredModel[], sizes: ReadonlyMap<string, number>): AiDiscoveredModel[];
|
|
339
|
+
/** Ollama `GET /api/tags`: the model names to ask `/api/show` about. */
|
|
340
|
+
declare function parseOllamaTags(body: unknown): string[];
|
|
341
|
+
/**
|
|
342
|
+
* Ollama `POST /api/show` for one model: `capabilities` (`completion`,
|
|
343
|
+
* `vision`, `tools`, `embedding`, `insert`) and the context length under
|
|
344
|
+
* `model_info["<arch>.context_length"]`.
|
|
345
|
+
*/
|
|
346
|
+
declare function parseOllamaShow(name: string, body: unknown): AiDiscoveredModel;
|
|
347
|
+
/** Which Ollama models are loaded right now, from `GET /api/ps` (optional). */
|
|
348
|
+
declare function parseOllamaRunning(body: unknown): Set<string>;
|
|
349
|
+
/**
|
|
350
|
+
* Anthropic `GET /v1/models`: `display_name`, `max_input_tokens` (the context
|
|
351
|
+
* window) and the `capabilities` tree (`image_input.supported`,
|
|
352
|
+
* `structured_outputs.supported`). Tools are assumed on.
|
|
353
|
+
*/
|
|
354
|
+
declare function parseAnthropicModels(body: unknown): AiDiscoveredModel[];
|
|
355
|
+
/** Anthropic pages its list; the next `after_id`, or null on the last page. */
|
|
356
|
+
declare function anthropicNextPage(body: unknown): string | null;
|
|
357
|
+
/** The URL detection GETs: `detectPath` under the server root (no `/v1`), else `{baseUrl}/models`. */
|
|
358
|
+
declare function localDetectUrl(baseUrl: string, detectPath?: string): string;
|
|
359
|
+
/**
|
|
360
|
+
* How many models a detection answer lists: `data` (OpenAI-style, LM Studio)
|
|
361
|
+
* or `models` (Ollama). LM Studio's embedding models are left out. Undefined
|
|
362
|
+
* for a body of neither shape (the server answered, the count is unknown).
|
|
363
|
+
*/
|
|
364
|
+
declare function countDetectedModels(body: unknown): number | undefined;
|
|
365
|
+
/** What a host needs to probe one detection candidate. */
|
|
366
|
+
interface AiLocalDetectTarget {
|
|
367
|
+
presetId: string;
|
|
368
|
+
baseUrl: string;
|
|
369
|
+
/** The GET. */
|
|
370
|
+
url: string;
|
|
371
|
+
/** For the auth header shape. */
|
|
372
|
+
kind: AiEngineKind;
|
|
373
|
+
/** Where to read a key, only when `baseUrl` is the preset's own address. */
|
|
374
|
+
apiKeyEnv?: string;
|
|
375
|
+
apiKeyFile?: AiApiKeyFile;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* A detection candidate checked against the presets. Null for an unknown or
|
|
379
|
+
* cloud preset, Custom, or an address that is not http(s). A key source
|
|
380
|
+
* (oMLX's key file) comes along only when the address is the preset's own,
|
|
381
|
+
* so a key never goes anywhere the user did not set up.
|
|
382
|
+
*/
|
|
383
|
+
declare function localDetectTarget(candidate: {
|
|
384
|
+
presetId: string;
|
|
385
|
+
baseUrl: string;
|
|
386
|
+
}, presets?: readonly AiEnginePreset[]): AiLocalDetectTarget | null;
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* The settings before the host sends any. Its own module so the editor can
|
|
390
|
+
* start from them without loading the rest of the AI layer (see `ai-lite.ts`).
|
|
391
|
+
*/
|
|
392
|
+
|
|
393
|
+
/** The advanced docs options before the user changes any. */
|
|
394
|
+
declare function defaultAiDocsOptions(): AiDocsOptions;
|
|
395
|
+
declare function defaultAiSettings(): AiSettings;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Task routing: which engine and model serve a feature's request. Pure and
|
|
399
|
+
* cheap, so the editor recomputes it on every request from the settings, the
|
|
400
|
+
* status cache of the last `listEngines`, and the consent answers so far.
|
|
401
|
+
*
|
|
402
|
+
* 1. The master switch and the feature must be on.
|
|
403
|
+
* 2. Route = `routing.tasks[task]`, else `routing.default`.
|
|
404
|
+
* 3. A candidate is usable when the engine exists, is enabled and of a known
|
|
405
|
+
* kind (a host kind such as `vscode-lm` only when the host lists a status
|
|
406
|
+
* for it); the model exists, is enabled and not `missing`; the model has every
|
|
407
|
+
* capability the task and feature need; the engine is local, or cloud with
|
|
408
|
+
* `allowCloud` on and consent not refused; and its last probe did not fail.
|
|
409
|
+
* 4. Otherwise `routing.fallback`: `none` stops, `default` tries the default
|
|
410
|
+
* route, `anyLocal` then every local engine/model in registry order, `any`
|
|
411
|
+
* then cloud engines through the same gate. Fallback never moves a local
|
|
412
|
+
* route to a cloud engine unless the mode is `any`.
|
|
413
|
+
* 5. The result names the pair and whether a fallback served it.
|
|
414
|
+
*
|
|
415
|
+
* A task may also `prefers` capabilities (JSON output for `structure`, a big
|
|
416
|
+
* context for `summarize`). That is never a rejection reason. The chosen
|
|
417
|
+
* route is honoured as is; when a fallback has several usable candidates in
|
|
418
|
+
* the same tier (the default route and the local engines, then the cloud
|
|
419
|
+
* ones), those meeting the preference come first, in registry order.
|
|
420
|
+
*
|
|
421
|
+
* Consent: a cloud candidate nobody has asked about yet is usable with
|
|
422
|
+
* `needsConsent: true`; the caller asks (`host.ai.requestCloudConsent`),
|
|
423
|
+
* records the answer and resolves again. A refusal (`false`) makes the
|
|
424
|
+
* engine unusable, which lets the fallback run.
|
|
425
|
+
*/
|
|
426
|
+
|
|
427
|
+
/** Consent answers by engine id: true granted, false refused ("Not now"), absent never asked. */
|
|
428
|
+
type AiConsentState = Readonly<Record<string, boolean | undefined>>;
|
|
429
|
+
type AiUnusableCode = 'aiDisabled' | 'featureDisabled' | 'noRoute' | 'engineMissing' | 'engineDisabled' | 'engineUnsupported' | 'modelMissing' | 'modelDisabled' | 'modelNotListed' | 'capability' | 'cloudBlocked' | 'consentDenied' | 'probeFailed';
|
|
430
|
+
interface AiRouteOk {
|
|
431
|
+
engineId: string;
|
|
432
|
+
modelId: string;
|
|
433
|
+
viaFallback: boolean;
|
|
434
|
+
/** Why the preferred route was passed over, when `viaFallback`. */
|
|
435
|
+
reason?: string;
|
|
436
|
+
/** The task that was routed. */
|
|
437
|
+
task: string;
|
|
438
|
+
isLocal: boolean;
|
|
439
|
+
/** Cloud engine with no consent answer yet: ask before sending. */
|
|
440
|
+
needsConsent: boolean;
|
|
441
|
+
}
|
|
442
|
+
interface AiRouteUnusable {
|
|
443
|
+
unusable: AiUnusableCode;
|
|
444
|
+
/** Tooltip copy, e.g. "oMLX (Mac Studio) failed its last connection test". */
|
|
445
|
+
reason: string;
|
|
446
|
+
task?: string;
|
|
447
|
+
/** The engine the rejection is about (the routed one), when it is about one engine. */
|
|
448
|
+
engineId?: string;
|
|
449
|
+
}
|
|
450
|
+
type AiRouteResolution = AiRouteOk | AiRouteUnusable;
|
|
451
|
+
declare function isRouteUsable(r: AiRouteResolution): r is AiRouteOk;
|
|
452
|
+
interface ResolveRouteOptions {
|
|
453
|
+
feature: string;
|
|
454
|
+
/** Route this task instead of the feature's. */
|
|
455
|
+
task?: string;
|
|
456
|
+
/** Inline suggestions: reject every cloud engine (`cloudBlocked`), whatever Allow cloud and consent say. */
|
|
457
|
+
localOnly?: boolean;
|
|
458
|
+
}
|
|
459
|
+
declare function resolveRoute(settings: AiSettings, statuses: readonly AiEngineStatus[], consent: AiConsentState, opts: ResolveRouteOptions): AiRouteResolution;
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Server-sent events, reassembled from arbitrary network chunks (a line, or
|
|
463
|
+
* a multi-byte character, may be split across chunks; the host decodes bytes
|
|
464
|
+
* with a streaming `TextDecoder` before pushing text here), plus readers for
|
|
465
|
+
* the two completion stream shapes:
|
|
466
|
+
*
|
|
467
|
+
* - OpenAI Chat Completions (OpenAI, oMLX, LM Studio, Ollama `/v1`, vLLM,
|
|
468
|
+
* llama.cpp): `data: {choices:[{delta:{content}}]}` chunks ending in
|
|
469
|
+
* `data: [DONE]`. `reasoning_content` deltas are thinking, not text.
|
|
470
|
+
* - Anthropic Messages: `message_start`, `content_block_delta` with
|
|
471
|
+
* `text_delta`, `message_delta` with `stop_reason` and usage, `message_stop`,
|
|
472
|
+
* and an `error` event.
|
|
473
|
+
*
|
|
474
|
+
* A stream that ends without its terminator (`[DONE]` or `message_stop`) is
|
|
475
|
+
* truncated: `finishReason: 'error'`.
|
|
476
|
+
*/
|
|
477
|
+
|
|
478
|
+
interface SseEvent {
|
|
479
|
+
/** `event:` field; absent means the default `message`. */
|
|
480
|
+
event?: string;
|
|
481
|
+
data: string;
|
|
482
|
+
id?: string;
|
|
483
|
+
}
|
|
484
|
+
/** Incremental SSE parser (WHATWG event-stream rules: CRLF/CR/LF, `:` comments, multi-line `data`). */
|
|
485
|
+
declare class SseParser {
|
|
486
|
+
private buffer;
|
|
487
|
+
private started;
|
|
488
|
+
private dataLines;
|
|
489
|
+
private eventName;
|
|
490
|
+
private lastId;
|
|
491
|
+
/** Feed decoded text; returns the events it completed. */
|
|
492
|
+
push(chunk: string): SseEvent[];
|
|
493
|
+
/** The stream closed: flush a final event that had no trailing blank line. */
|
|
494
|
+
end(): SseEvent[];
|
|
495
|
+
private line;
|
|
496
|
+
}
|
|
497
|
+
/** Where a completion stream ended up. */
|
|
498
|
+
interface StreamOutcome {
|
|
499
|
+
text: string;
|
|
500
|
+
finishReason: AiFinishReason;
|
|
501
|
+
usage?: AiUsage;
|
|
502
|
+
/** The provider's own stop reason (`end_turn`, `length`, `refusal`...). */
|
|
503
|
+
stopReason?: string;
|
|
504
|
+
/** A stream-level error event, mapped to a code. */
|
|
505
|
+
error?: {
|
|
506
|
+
code: AiErrorCode;
|
|
507
|
+
message: string;
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
/** One completion stream: push decoded chunks, get text deltas back, then `finish`. */
|
|
511
|
+
interface CompletionStreamReader {
|
|
512
|
+
push(chunk: string): string[];
|
|
513
|
+
finish(): StreamOutcome;
|
|
514
|
+
/**
|
|
515
|
+
* Output seen so far, answer plus reasoning, in UTF-16 units: above zero
|
|
516
|
+
* once the model starts generating (the host's first-token time), and the
|
|
517
|
+
* basis of a token estimate when the engine reports no usage.
|
|
518
|
+
*/
|
|
519
|
+
outputChars(): number;
|
|
520
|
+
}
|
|
521
|
+
/** Chat Completions stream (`openai-compatible`, `lmstudio`, `ollama`, `openai`). */
|
|
522
|
+
declare function createOpenAiStreamReader(): CompletionStreamReader;
|
|
523
|
+
/** Anthropic Messages stream. */
|
|
524
|
+
declare function createAnthropicStreamReader(): CompletionStreamReader;
|
|
525
|
+
/** The reader for an engine kind. */
|
|
526
|
+
declare function createCompletionStreamReader(kind: AiEngineKind): CompletionStreamReader;
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* How long a result took and how fast it came: the popover footer's
|
|
530
|
+
* "2.4 s · 38 tok/s", its tooltip, and the hosts' request log. Hosts measure
|
|
531
|
+
* (`AiCompletionResult.timing`); this only does the arithmetic and the words.
|
|
532
|
+
*
|
|
533
|
+
* Output tokens are the engine's own count when it reports one (OpenAI-style
|
|
534
|
+
* `usage` with `stream_options.include_usage`, Anthropic `message_delta`),
|
|
535
|
+
* else about one token per four characters of output, marked approximate
|
|
536
|
+
* ("~38 tok/s"). Speed is output tokens over the time from the first token to
|
|
537
|
+
* the end; without a first-token time, over the whole request.
|
|
538
|
+
*
|
|
539
|
+
* Desktop's `request_log.rs` ports `aiTimingLogParts`; keep them in step.
|
|
540
|
+
*/
|
|
541
|
+
|
|
542
|
+
/** Characters per token for the estimate when the engine reports no usage. */
|
|
543
|
+
declare const AI_CHARS_PER_TOKEN = 4;
|
|
544
|
+
interface AiResultStats {
|
|
545
|
+
totalMs: number;
|
|
546
|
+
firstTokenMs?: number;
|
|
547
|
+
outputTokens?: number;
|
|
548
|
+
inputTokens?: number;
|
|
549
|
+
/** `outputTokens` (and so the speed) is estimated from the characters. */
|
|
550
|
+
approximate: boolean;
|
|
551
|
+
tokensPerSecond?: number;
|
|
552
|
+
}
|
|
553
|
+
/** The numbers behind the footer, or null when the host measured nothing (an older host). */
|
|
554
|
+
declare function aiResultStats(result: Pick<AiCompletionResult, 'text' | 'usage' | 'timing'>): AiResultStats | null;
|
|
555
|
+
/** "0.8 s", "2.4 s", "12 s". */
|
|
556
|
+
declare function formatAiSeconds(ms: number): string;
|
|
557
|
+
/** "38 tok/s", "~38 tok/s", "4.5 tok/s". */
|
|
558
|
+
declare function formatAiTokensPerSecond(tps: number, approximate: boolean): string;
|
|
559
|
+
/** The footer text: "2.4 s · 38 tok/s", or just "2.4 s" with no speed. */
|
|
560
|
+
declare function aiResultStatsText(stats: AiResultStats): string;
|
|
561
|
+
/** The footer tooltip, one fact per line. */
|
|
562
|
+
declare function aiResultStatsTitle(stats: AiResultStats): string;
|
|
563
|
+
/** The request log's additions: `first token N ms` and the speed, when measured. */
|
|
564
|
+
declare function aiTimingLogParts(result: Pick<AiCompletionResult, 'text' | 'usage' | 'timing'>): string[];
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Request building per engine kind: endpoint URLs, auth headers and the
|
|
568
|
+
* completion and embedding bodies. Pure, so the VS Code adapters and the Rust
|
|
569
|
+
* port produce identical requests (checked against the same fixtures).
|
|
570
|
+
*
|
|
571
|
+
* Bodies:
|
|
572
|
+
* - Chat Completions kinds: `{ model, messages, stream, max_tokens,
|
|
573
|
+
* temperature, response_format }`; `openai` uses `max_completion_tokens`
|
|
574
|
+
* and asks for usage in the stream. OpenAI reasoning models (o-series,
|
|
575
|
+
* GPT-5 onwards) get no `temperature`: they accept only the default.
|
|
576
|
+
* - `anthropic`: Messages API, system prompts lifted into `system`,
|
|
577
|
+
* `max_tokens` always set (required). No sampling parameters: API version
|
|
578
|
+
* 2026-10-21 rejects `temperature`.
|
|
579
|
+
*
|
|
580
|
+
* `model.requestOverrides` merge last, deeply (objects merge, everything else
|
|
581
|
+
* replaces), so they win (OPEN-6); then the request's own `requestOverrides`
|
|
582
|
+
* (a per-request thinking level) merge over those the same way.
|
|
583
|
+
*
|
|
584
|
+
* A 400 that names a parameter the model does not take
|
|
585
|
+
* (`unsupportedRequestParam`) is retried once without it
|
|
586
|
+
* (`withoutRequestParam`); the hosts remember it per engine and model for
|
|
587
|
+
* the session.
|
|
588
|
+
*/
|
|
589
|
+
|
|
590
|
+
/** `anthropic-version` sent by the Anthropic adapter. */
|
|
591
|
+
declare const ANTHROPIC_API_VERSION = "2026-10-21";
|
|
592
|
+
/** `max_tokens` for Anthropic when the request sets none (the API requires it). */
|
|
593
|
+
declare const ANTHROPIC_DEFAULT_MAX_TOKENS = 4096;
|
|
594
|
+
/** Appended to the system prompt for `format: 'json'` on kinds with no schema-free JSON mode. */
|
|
595
|
+
declare const JSON_ONLY_INSTRUCTION = "Respond with a single valid JSON value and nothing else.";
|
|
596
|
+
type Json$1 = Record<string, unknown>;
|
|
597
|
+
interface AiEngineEndpoints {
|
|
598
|
+
/** Streaming completion. */
|
|
599
|
+
completions: string;
|
|
600
|
+
/** Model list used for Test connection and bare discovery. */
|
|
601
|
+
models: string;
|
|
602
|
+
/** Embeddings, absent for kinds without them. */
|
|
603
|
+
embeddings?: string;
|
|
604
|
+
/** Native discovery endpoint (LM Studio `/api/v0/models`, Ollama `/api/tags`). */
|
|
605
|
+
nativeModels?: string;
|
|
606
|
+
/** Ollama `POST /api/show` (body `{ model }`). */
|
|
607
|
+
nativeShow?: string;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* URLs for an engine. OpenAI-style base URLs include `/v1`
|
|
611
|
+
* (`http://127.0.0.1:8000/v1`); for LM Studio and Ollama a base without it
|
|
612
|
+
* gets `/v1` added for completions, and their native endpoints hang off the
|
|
613
|
+
* root. Anthropic's base is the host (`https://api.anthropic.com`), with or
|
|
614
|
+
* without `/v1`.
|
|
615
|
+
*/
|
|
616
|
+
declare function engineEndpoints(engine: Pick<AiEngineConfig, 'kind' | 'baseUrl'>): AiEngineEndpoints;
|
|
617
|
+
/** Headers for every request to the engine (JSON content type included). */
|
|
618
|
+
declare function buildAuthHeaders(kind: AiEngineKind, apiKey: string | null): Record<string, string>;
|
|
619
|
+
/** Deep merge: nested plain objects merge, anything else in `overrides` replaces. Inputs are not mutated. */
|
|
620
|
+
declare function mergeRequestOverrides(body: Json$1, overrides: Json$1 | undefined): Json$1;
|
|
621
|
+
/** Standard base64 with padding, no line breaks. No `Buffer` or `btoa`, so it runs in every host. */
|
|
622
|
+
declare function bytesToBase64(bytes: Uint8Array): string;
|
|
623
|
+
/** Inverse of `bytesToBase64`; ignores whitespace and accepts URL-safe characters. */
|
|
624
|
+
declare function base64ToBytes(b64: string): Uint8Array;
|
|
625
|
+
interface BuildCompletionOptions {
|
|
626
|
+
/** Default true. */
|
|
627
|
+
stream?: boolean;
|
|
628
|
+
}
|
|
629
|
+
/** The JSON body for one completion against `model` on an engine of `kind`. */
|
|
630
|
+
declare function buildCompletionBody(kind: AiEngineKind, model: Pick<AiModelConfig, 'id' | 'requestOverrides'>, req: AiCompletionRequest, opts?: BuildCompletionOptions): Json$1;
|
|
631
|
+
/**
|
|
632
|
+
* Cloud hosts of the `openai-compatible` kind that take
|
|
633
|
+
* `stream_options.include_usage` (OpenRouter, Gemini, Groq, DeepSeek).
|
|
634
|
+
* Mistral and unknown cloud hosts are left out: a strict server may reject
|
|
635
|
+
* a field it does not know.
|
|
636
|
+
*/
|
|
637
|
+
declare const STREAM_USAGE_HOSTS: readonly string[];
|
|
638
|
+
/**
|
|
639
|
+
* Whether a streamed completion may ask for usage in the stream. `openai`,
|
|
640
|
+
* LM Studio and Ollama take it; on `openai-compatible`, a server on this
|
|
641
|
+
* machine or network (oMLX, vLLM, llama.cpp all take it) or a host in
|
|
642
|
+
* `STREAM_USAGE_HOSTS`. Anthropic reports usage without being asked.
|
|
643
|
+
*/
|
|
644
|
+
declare function streamUsageSupported(engine: Pick<AiEngineConfig, 'kind' | 'baseUrl'>): boolean;
|
|
645
|
+
/**
|
|
646
|
+
* `body` asking for usage in the stream (`stream_options.include_usage`)
|
|
647
|
+
* when the engine takes it, the body streams and the overrides did not set
|
|
648
|
+
* `stream_options` themselves. Hosts apply it after `buildCompletionBody`.
|
|
649
|
+
*/
|
|
650
|
+
declare function withStreamUsage(body: Json$1, engine: Pick<AiEngineConfig, 'kind' | 'baseUrl'>): Json$1;
|
|
651
|
+
/**
|
|
652
|
+
* Body fields a server may reject for one model while taking them for
|
|
653
|
+
* others. Only these are retried: anything else in a 400 is a real error.
|
|
654
|
+
*/
|
|
655
|
+
declare const RETRYABLE_REQUEST_PARAMS: readonly string[];
|
|
656
|
+
/**
|
|
657
|
+
* The parameter a failed completion was rejected for, or null. Only a 400
|
|
658
|
+
* whose error says the parameter (or its value) is unsupported counts:
|
|
659
|
+
* OpenAI's `{ error: { param, code: "unsupported_value" | "unsupported_parameter", message } }`,
|
|
660
|
+
* or any body whose message quotes one of `RETRYABLE_REQUEST_PARAMS` next to
|
|
661
|
+
* that wording (the first one quoted, so "Use 'max_completion_tokens'
|
|
662
|
+
* instead" does not count).
|
|
663
|
+
*/
|
|
664
|
+
declare function unsupportedRequestParam(status: number, bodyText: string): string | null;
|
|
665
|
+
/**
|
|
666
|
+
* `body` without a parameter the server rejected: `max_tokens` and
|
|
667
|
+
* `max_completion_tokens` become each other (the documented replacement),
|
|
668
|
+
* anything else is dropped. Null when the body does not carry it, so there
|
|
669
|
+
* is nothing to retry.
|
|
670
|
+
*/
|
|
671
|
+
declare function withoutRequestParam(body: Json$1, param: string): Json$1 | null;
|
|
672
|
+
/** Embeddings body (`POST .../embeddings`), or null for kinds without an embeddings endpoint. */
|
|
673
|
+
declare function buildEmbeddingsBody(kind: AiEngineKind, model: Pick<AiModelConfig, 'id' | 'requestOverrides'>, texts: string[]): Json$1 | null;
|
|
674
|
+
/** Vectors from an OpenAI-style embeddings response, in input order. */
|
|
675
|
+
declare function parseEmbeddingsResponse(body: unknown): number[][];
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* `AiCompletionRequest` over a JSON channel (VS Code `postMessage`, Tauri
|
|
679
|
+
* `invoke`): image bytes travel as base64, the same way `savePastedImage`
|
|
680
|
+
* sends pasted images.
|
|
681
|
+
*/
|
|
682
|
+
|
|
683
|
+
type AiWireMessagePart = {
|
|
684
|
+
type: 'text';
|
|
685
|
+
text: string;
|
|
686
|
+
} | {
|
|
687
|
+
type: 'image';
|
|
688
|
+
mime: string;
|
|
689
|
+
dataBase64: string;
|
|
690
|
+
};
|
|
691
|
+
interface AiWireMessage {
|
|
692
|
+
role: AiMessage['role'];
|
|
693
|
+
content: string | AiWireMessagePart[];
|
|
694
|
+
}
|
|
695
|
+
type AiWireCompletionRequest = Omit<AiCompletionRequest, 'messages'> & {
|
|
696
|
+
messages: AiWireMessage[];
|
|
697
|
+
};
|
|
698
|
+
declare function toWireCompletionRequest(req: AiCompletionRequest): AiWireCompletionRequest;
|
|
699
|
+
declare function fromWireCompletionRequest(wire: AiWireCompletionRequest): AiCompletionRequest;
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* `AiError` helpers and the user-facing copy for each `AiErrorCode`, kept in
|
|
703
|
+
* one place so the rewrite popover, the AI tab and the indicator say the same
|
|
704
|
+
* thing. Hosts reject with `{ code, message }`; `message` is the technical
|
|
705
|
+
* detail, the copy here is what the user reads first.
|
|
706
|
+
*/
|
|
707
|
+
|
|
708
|
+
interface AiErrorCopy {
|
|
709
|
+
title: string;
|
|
710
|
+
message: string;
|
|
711
|
+
/**
|
|
712
|
+
* Suggested next step, when there is one. `openEngine`: the failing
|
|
713
|
+
* engine's own page. `chooseModel`: the same page, to pick another model.
|
|
714
|
+
*/
|
|
715
|
+
action?: 'openSettings' | 'openEngine' | 'chooseModel' | 'retry' | 'shortenSelection';
|
|
716
|
+
}
|
|
717
|
+
declare const AI_ERROR_COPY: Readonly<Record<AiErrorCode, AiErrorCopy>>;
|
|
718
|
+
/**
|
|
719
|
+
* A stream that stopped at its token limit (`finishReason: 'length'`). A
|
|
720
|
+
* thinking model counts its reasoning against that limit, so with thinking
|
|
721
|
+
* on the advice is to lower Thinking. `empty`: no answer text came back at
|
|
722
|
+
* all (shown as the error); otherwise the answer is shown with `message` as
|
|
723
|
+
* a note.
|
|
724
|
+
*/
|
|
725
|
+
declare function aiLengthLimitCopy(opts: {
|
|
726
|
+
thinking: boolean;
|
|
727
|
+
empty: boolean;
|
|
728
|
+
}): AiErrorCopy;
|
|
729
|
+
declare function isAiErrorCode(v: unknown): v is AiErrorCode;
|
|
730
|
+
declare function isAiError(v: unknown): v is AiError;
|
|
731
|
+
declare function makeAiError(code: AiErrorCode, message?: string): AiError;
|
|
732
|
+
/** Any rejection value as an `AiError`: an `AiError` as is, an `AbortError` as `cancelled`, the rest `unknown`. */
|
|
733
|
+
declare function toAiError(err: unknown): AiError;
|
|
734
|
+
/**
|
|
735
|
+
* The copy for `code`. With `engine` (the display name of the engine the
|
|
736
|
+
* request went to), an engine problem names it and its action is
|
|
737
|
+
* `openEngine` (`chooseModel` for a model that did not load): the popover's
|
|
738
|
+
* button opens that engine instead of the AI tab. `model` names the model
|
|
739
|
+
* where the copy has room for it.
|
|
740
|
+
*/
|
|
741
|
+
declare function aiErrorCopy(code: AiErrorCode, opts?: {
|
|
742
|
+
engine?: string;
|
|
743
|
+
model?: string;
|
|
744
|
+
}): AiErrorCopy;
|
|
745
|
+
/**
|
|
746
|
+
* A local server that could not load the model: LM Studio's `Failed to load
|
|
747
|
+
* model "x"`, llama.cpp's `error loading model`, Ollama's `llama runner
|
|
748
|
+
* process has terminated` and its out-of-memory refusal.
|
|
749
|
+
*/
|
|
750
|
+
declare const MODEL_LOAD_FAILED_PHRASES: readonly string[];
|
|
751
|
+
/**
|
|
752
|
+
* Map an HTTP failure to a code (shared by both hosts): 401/403 auth, 404
|
|
753
|
+
* modelNotFound, a body that says the model failed to load modelLoadFailed,
|
|
754
|
+
* 413 or a 400 that mentions the context contextOverflow, 429 and 529
|
|
755
|
+
* rateLimited, 502/503/504 unreachable, everything else unknown.
|
|
756
|
+
*/
|
|
757
|
+
declare function aiErrorCodeFromHttp(status: number, bodyText?: string): AiErrorCode;
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Per-model thinking level (Off / Low / Medium / High) mapped onto each
|
|
761
|
+
* engine kind's request fields, stored in the model's `requestOverrides`
|
|
762
|
+
* (OPEN-6) so the adapters need no change: they merge the overrides into
|
|
763
|
+
* every request body last.
|
|
764
|
+
*
|
|
765
|
+
* Mappings:
|
|
766
|
+
* - `openai` and cloud `openai-compatible` (Gemini, Groq, OpenRouter...):
|
|
767
|
+
* `reasoning_effort` `none` / `low` / `medium` / `high`. Off is stored as
|
|
768
|
+
* `none`; `buildCompletionBody` swaps it for the lowest value an OpenAI
|
|
769
|
+
* reasoning model accepts (`openAiOffReasoningEffort`).
|
|
770
|
+
* - local `openai-compatible` (oMLX, vLLM, llama.cpp): tiers through
|
|
771
|
+
* `reasoning_effort`; Off through `chat_template_kwargs.enable_thinking:
|
|
772
|
+
* false`, which the chat template honours (oMLX ignores
|
|
773
|
+
* `reasoning_effort: "none"`).
|
|
774
|
+
* - `lmstudio`: on/off only, `reasoning_effort` `none` or `medium`.
|
|
775
|
+
* - `ollama`: `reasoning_effort` `none` / `low` / `medium` / `high` (its
|
|
776
|
+
* OpenAI-compatible endpoint maps it onto `think`).
|
|
777
|
+
* - `vscode-lm`: none (VS Code's language model API has no such field).
|
|
778
|
+
* - `anthropic`: adaptive thinking with `output_config.effort` `low` /
|
|
779
|
+
* `medium` / `high`. No Off: current models reject `thinking: disabled`
|
|
780
|
+
* (Opus 5.5, Sonnet 5.5, Fable), so Low is the lightest setting.
|
|
781
|
+
*
|
|
782
|
+
* "Model default" (no level) removes the kind's thinking fields and leaves
|
|
783
|
+
* everything else in `requestOverrides` alone.
|
|
784
|
+
*
|
|
785
|
+
* Per-task default (Q6): with the dial on Default and no level set on the
|
|
786
|
+
* model, MikeDown picks the level for the job (`taskThinkingDefault`): Off
|
|
787
|
+
* for the short edits (every rewrite variant, translate, suggest title,
|
|
788
|
+
* suggest properties), none for the rest (explain, summarize), so the model
|
|
789
|
+
* decides there. An explicit dial choice, or a level set on the model, wins
|
|
790
|
+
* (`resolveThinkingLevel`).
|
|
791
|
+
*/
|
|
792
|
+
type AiThinkingLevel = 'off' | 'low' | 'medium' | 'high';
|
|
793
|
+
interface AiThinkingChoice {
|
|
794
|
+
level: AiThinkingLevel;
|
|
795
|
+
label: string;
|
|
796
|
+
}
|
|
797
|
+
interface AiThinkingOptions {
|
|
798
|
+
/** The engine's locality is local (changes the Off mapping for `openai-compatible`). */
|
|
799
|
+
local?: boolean;
|
|
800
|
+
}
|
|
801
|
+
type Json = Record<string, unknown>;
|
|
802
|
+
/** The levels a kind can express, in order. Empty means no known mapping: hide the control. */
|
|
803
|
+
declare function thinkingChoices(kind: string): AiThinkingChoice[];
|
|
804
|
+
/**
|
|
805
|
+
* The level a kind uses for `level`: the level itself when the kind has it,
|
|
806
|
+
* else the nearest one (Off on Anthropic is Low, any tier on LM Studio is
|
|
807
|
+
* On). Null for no level or a kind with no mapping.
|
|
808
|
+
*/
|
|
809
|
+
declare function nearestThinkingLevel(kind: string, level: AiThinkingLevel | null): AiThinkingLevel | null;
|
|
810
|
+
/**
|
|
811
|
+
* What a request is for, as far as its default thinking level goes: a
|
|
812
|
+
* feature id (`rewrite`, `title`, `frontmatter`, `summary`) or a job that
|
|
813
|
+
* shares one (`explain` and `translate` run under `rewrite`).
|
|
814
|
+
*/
|
|
815
|
+
type AiThinkingJob = string;
|
|
816
|
+
/**
|
|
817
|
+
* The level MikeDown picks for a job when the dial is on Default (Q6): Off
|
|
818
|
+
* for the short edits and the docs chat's `navigate` hops, null (the model's
|
|
819
|
+
* own) for everything else, the docs chat's `answer` call included.
|
|
820
|
+
*/
|
|
821
|
+
declare function taskThinkingDefault(job: string | undefined): AiThinkingLevel | null;
|
|
822
|
+
interface ResolvedThinkingLevel {
|
|
823
|
+
/** The level to send (already the kind's nearest), or null to send none. */
|
|
824
|
+
level: AiThinkingLevel | null;
|
|
825
|
+
/** True when `level` is the job's default rather than the user's choice. */
|
|
826
|
+
auto: boolean;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* The level a request sends: the user's explicit choice (the dial) first,
|
|
830
|
+
* then nothing when the model has its own level set (`configured`), then the
|
|
831
|
+
* job's default (mapped to the kind's nearest: Off on Anthropic is Low).
|
|
832
|
+
* The explicit level is sent as given (`thinkingLevelFor` already maps a
|
|
833
|
+
* remembered one); one the kind cannot express sends nothing.
|
|
834
|
+
*/
|
|
835
|
+
declare function resolveThinkingLevel(kind: string, explicit: AiThinkingLevel | null, configured: AiThinkingLevel | null, job: string | undefined, opts?: {
|
|
836
|
+
modelId?: string;
|
|
837
|
+
}): ResolvedThinkingLevel;
|
|
838
|
+
/** The request fields for one level, or `{}` when the kind cannot express it. */
|
|
839
|
+
declare function thinkingOverrides(kind: string, level: AiThinkingLevel, opts?: AiThinkingOptions): Json;
|
|
840
|
+
/** The level `overrides` express for this kind, or null for "model default" (or a value the menu has no entry for). */
|
|
841
|
+
declare function thinkingLevelFrom(kind: string, overrides: Json | undefined): AiThinkingLevel | null;
|
|
842
|
+
/**
|
|
843
|
+
* `requestOverrides` with the thinking level set (null: model default).
|
|
844
|
+
* Other keys, nested ones included, are kept. Undefined when nothing is left.
|
|
845
|
+
*/
|
|
846
|
+
declare function withThinkingLevel(kind: string, overrides: Json | undefined, level: AiThinkingLevel | null, opts?: AiThinkingOptions): Json | undefined;
|
|
847
|
+
/**
|
|
848
|
+
* Output tokens a thinking model may spend reasoning before it answers, by
|
|
849
|
+
* level. A thinking model counts its reasoning against `max_tokens`, so an
|
|
850
|
+
* answer budget alone can be used up before the first visible word. Sized
|
|
851
|
+
* for local reasoning models (Qwen3 class), which often think 2,000 to
|
|
852
|
+
* 4,000 tokens even on a one-paragraph edit. It is a ceiling, not a spend.
|
|
853
|
+
*/
|
|
854
|
+
declare const THINKING_HEADROOM_TOKENS: Readonly<Record<AiThinkingLevel, number>>;
|
|
855
|
+
/** "Model default" (no level set): the model may think, at a level we cannot see. */
|
|
856
|
+
declare const THINKING_DEFAULT_HEADROOM_TOKENS: number;
|
|
857
|
+
/** The most `thinkingMaxTokens` raises a request to (a larger answer budget is kept as is). */
|
|
858
|
+
declare const THINKING_MAX_TOKENS_CAP = 12288;
|
|
859
|
+
interface ThinkingBudgetOptions {
|
|
860
|
+
/** The model's context window, when known. */
|
|
861
|
+
contextTokens?: number | null;
|
|
862
|
+
/** Estimated prompt tokens; with `contextTokens`, the headroom never pushes past the window. */
|
|
863
|
+
promptTokens?: number;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* The `max_tokens` to send for an answer budget of `answerTokens` on a model
|
|
867
|
+
* that can think: the answer plus reasoning headroom for the effective level
|
|
868
|
+
* (null: model default, counted as thinking). Off adds nothing. The raise is
|
|
869
|
+
* capped at `THINKING_MAX_TOKENS_CAP` and at what is left of the context
|
|
870
|
+
* window, and never goes below the answer budget.
|
|
871
|
+
*/
|
|
872
|
+
declare function thinkingMaxTokens(answerTokens: number, level: AiThinkingLevel | null, opts?: ThinkingBudgetOptions): number;
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Selection rewrite prompt (the `rewrite` feature). Pure: builds the chat
|
|
876
|
+
* messages from the selected markdown and a little context, and cleans what
|
|
877
|
+
* the model sends back before it is previewed or parsed into the document.
|
|
878
|
+
*
|
|
879
|
+
* Context budget (brainstorm constraint 8): the selection plus the text of
|
|
880
|
+
* the enclosing block before and after it, and the neighbouring block on each
|
|
881
|
+
* side, capped at `REWRITE_CONTEXT_CHARS` per side. Never the whole document.
|
|
882
|
+
*/
|
|
883
|
+
|
|
884
|
+
type AiRewriteVariant = 'shorter' | 'longer' | 'expand' | 'simpler' | 'fixFlow' | 'formal' | 'casual' | 'readingLevel' | 'translate' | 'explain' | 'custom';
|
|
885
|
+
/** Menu group a variant belongs to (the popover's choose phase renders one section per group). */
|
|
886
|
+
type AiRewriteGroup = 'length' | 'tone' | 'audience' | 'translate' | 'explain' | 'custom';
|
|
887
|
+
/** `replace` swaps the selection; `readOnly` only shows text (Explain) and can insert it below. */
|
|
888
|
+
type AiRewriteOutput = 'replace' | 'readOnly';
|
|
889
|
+
type AiAudience = 'general' | 'expert' | 'plain' | 'grade8' | 'esl';
|
|
890
|
+
interface AiAudienceDefinition {
|
|
891
|
+
id: AiAudience;
|
|
892
|
+
label: string;
|
|
893
|
+
/** Instruction line sent to the model for `readingLevel`. */
|
|
894
|
+
instruction: string;
|
|
895
|
+
}
|
|
896
|
+
declare const AI_AUDIENCES: readonly AiAudienceDefinition[];
|
|
897
|
+
declare function getAiAudience(id: string): AiAudienceDefinition | undefined;
|
|
898
|
+
interface AiRewriteVariantDefinition {
|
|
899
|
+
id: AiRewriteVariant;
|
|
900
|
+
/** Menu label. */
|
|
901
|
+
label: string;
|
|
902
|
+
/** Tooltip, where the label alone does not tell two variants apart (Longer, Add detail). */
|
|
903
|
+
title?: string;
|
|
904
|
+
/**
|
|
905
|
+
* The instruction line sent to the model. Empty for `custom` (the user's own
|
|
906
|
+
* words); `{language}` and `{audience}` are filled from `RewritePromptInput.params`.
|
|
907
|
+
*/
|
|
908
|
+
instruction: string;
|
|
909
|
+
group: AiRewriteGroup;
|
|
910
|
+
output: AiRewriteOutput;
|
|
911
|
+
/** The parameter this variant needs before it can run (a second-level picker supplies it). */
|
|
912
|
+
param?: 'language' | 'audience';
|
|
913
|
+
}
|
|
914
|
+
/** Order here is the menu order. */
|
|
915
|
+
declare const AI_REWRITE_VARIANTS: readonly AiRewriteVariantDefinition[];
|
|
916
|
+
declare function getAiRewriteVariant(id: string): AiRewriteVariantDefinition | undefined;
|
|
917
|
+
/** Context kept on each side of the selection, in characters (about 400 tokens). */
|
|
918
|
+
declare const REWRITE_CONTEXT_CHARS = 1600;
|
|
919
|
+
/** Longest custom instruction sent as is. */
|
|
920
|
+
declare const REWRITE_INSTRUCTION_MAX_CHARS = 1000;
|
|
921
|
+
interface RewritePromptInput {
|
|
922
|
+
variant: AiRewriteVariant;
|
|
923
|
+
/** The user's instruction for `custom` (ignored otherwise). */
|
|
924
|
+
instruction?: string;
|
|
925
|
+
/** The selected text as markdown. */
|
|
926
|
+
selection: string;
|
|
927
|
+
/** Markdown just before the selection (enclosing block, then the previous one). */
|
|
928
|
+
before?: string;
|
|
929
|
+
/** Markdown just after the selection. */
|
|
930
|
+
after?: string;
|
|
931
|
+
/** Values for the parameterised variants (`translate` needs `language`, `readingLevel` needs `audience`). */
|
|
932
|
+
params?: AiRewriteParams;
|
|
933
|
+
/**
|
|
934
|
+
* `document` for a whole-document run (`translate`): the passage is the body
|
|
935
|
+
* without its frontmatter and no context is sent. Default `selection`.
|
|
936
|
+
*/
|
|
937
|
+
scope?: AiRewriteScope;
|
|
938
|
+
}
|
|
939
|
+
interface AiRewriteParams {
|
|
940
|
+
language?: string;
|
|
941
|
+
audience?: AiAudience;
|
|
942
|
+
}
|
|
943
|
+
type AiRewriteScope = 'selection' | 'document';
|
|
944
|
+
/** Longest target language name sent (it is free text from "Other…"). */
|
|
945
|
+
declare const REWRITE_LANGUAGE_MAX_CHARS = 60;
|
|
946
|
+
declare const REWRITE_SYSTEM_PROMPT: string;
|
|
947
|
+
/**
|
|
948
|
+
* Appended to the system prompt for `translate`. It overrides the "write in the
|
|
949
|
+
* language of the passage" rule; the target language is named only in the
|
|
950
|
+
* instruction line.
|
|
951
|
+
*/
|
|
952
|
+
declare const TRANSLATE_SYSTEM_ADDENDUM: string;
|
|
953
|
+
/** Appended to the system prompt when the instruction asks for an explanation (the `explain` variant). */
|
|
954
|
+
declare const 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.";
|
|
955
|
+
/** The whole-document variant of the translation note. */
|
|
956
|
+
declare const 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.";
|
|
957
|
+
/** Neutralise a closing tag inside user text so it cannot end its block early. */
|
|
958
|
+
declare function escapeClosingTag(text: string, tag: string): string;
|
|
959
|
+
/** Context trimmed to the budget at the far end, so the part next to the selection survives. */
|
|
960
|
+
declare function trimRewriteContext(text: string, side: 'before' | 'after', max?: number): string;
|
|
961
|
+
/** Target language as typed, flattened to one short line (it ends up inside a prompt). */
|
|
962
|
+
declare function normalizeRewriteLanguage(language: string | undefined): string;
|
|
963
|
+
/**
|
|
964
|
+
* The instruction line for a variant. A parameterised variant whose parameter
|
|
965
|
+
* is missing or unknown falls back to `fixFlow`, like an empty `custom`.
|
|
966
|
+
*/
|
|
967
|
+
declare function rewriteInstruction(input: Pick<RewritePromptInput, 'variant' | 'instruction' | 'params'>): string;
|
|
968
|
+
/** The system prompt for a variant: the shared rules plus the translate or explain addendum. */
|
|
969
|
+
declare function rewriteSystemPrompt(variant: AiRewriteVariant, scope?: AiRewriteScope): string;
|
|
970
|
+
declare function buildRewriteMessages(input: RewritePromptInput): AiMessage[];
|
|
971
|
+
/** Output budget for `explain`: a short note, independent of the passage length. */
|
|
972
|
+
declare const EXPLAIN_MAX_TOKENS = 600;
|
|
973
|
+
/** Output budget: room for "longer" to double the passage, never below a short paragraph. */
|
|
974
|
+
declare function rewriteMaxTokens(selection: string, variant: AiRewriteVariant): number;
|
|
975
|
+
/**
|
|
976
|
+
* The text to show while it streams: reasoning blocks (`<think>…</think>`,
|
|
977
|
+
* which some local models put in the content) are hidden, including one that
|
|
978
|
+
* has not closed yet, and an echoed `<passage>` tag is dropped.
|
|
979
|
+
*/
|
|
980
|
+
declare function rewriteVisibleText(raw: string): string;
|
|
981
|
+
/**
|
|
982
|
+
* The final replacement: the visible text without a wrapping code fence or
|
|
983
|
+
* quotes the model added, trimmed, with the selection's own leading and
|
|
984
|
+
* trailing whitespace put back (a selection of "word " stays one word plus a
|
|
985
|
+
* space).
|
|
986
|
+
*/
|
|
987
|
+
declare function finalizeRewriteText(raw: string, selection: string): string;
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Explain prompt (the `explain` variant of the `rewrite` feature). Pure: builds
|
|
991
|
+
* the chat messages for a read-only note about the selected passage. The reply
|
|
992
|
+
* is shown in the popover and can be inserted below the selection; it never
|
|
993
|
+
* replaces anything.
|
|
994
|
+
*/
|
|
995
|
+
|
|
996
|
+
declare const EXPLAIN_SYSTEM_PROMPT: string;
|
|
997
|
+
interface ExplainPromptInput {
|
|
998
|
+
/** The selected text as markdown. */
|
|
999
|
+
selection: string;
|
|
1000
|
+
/** Markdown just before the selection. */
|
|
1001
|
+
before?: string;
|
|
1002
|
+
/** Markdown just after the selection. */
|
|
1003
|
+
after?: string;
|
|
1004
|
+
/** The document title (frontmatter `title:` or the first heading), if there is one. */
|
|
1005
|
+
title?: string;
|
|
1006
|
+
}
|
|
1007
|
+
declare function buildExplainMessages(input: ExplainPromptInput): AiMessage[];
|
|
1008
|
+
/** Output budget: a short note whatever the passage length. */
|
|
1009
|
+
declare function explainMaxTokens(): number;
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Markdown structure post-check. Pure: counts the structural pieces of a
|
|
1013
|
+
* passage and compares the original with a model's result, so translate and
|
|
1014
|
+
* tone rewrites can show a warning line before Accept when something was
|
|
1015
|
+
* dropped. A warning only; it never blocks Accept.
|
|
1016
|
+
*/
|
|
1017
|
+
interface StructureCounts {
|
|
1018
|
+
/** Headings by level, index 0 is level 1. */
|
|
1019
|
+
headings: [number, number, number, number, number, number];
|
|
1020
|
+
fencedCodeBlocks: number;
|
|
1021
|
+
inlineCodeSpans: number;
|
|
1022
|
+
/** URL of every link (inline, reference definition, autolink), in order. */
|
|
1023
|
+
links: string[];
|
|
1024
|
+
/** URL of every image, in order. */
|
|
1025
|
+
images: string[];
|
|
1026
|
+
listItems: number;
|
|
1027
|
+
tableRows: number;
|
|
1028
|
+
wikilinks: number;
|
|
1029
|
+
footnoteReferences: number;
|
|
1030
|
+
}
|
|
1031
|
+
interface StructureMismatch {
|
|
1032
|
+
kind: 'heading' | 'fencedCodeBlock' | 'inlineCode' | 'link' | 'image' | 'listItem' | 'tableRow' | 'wikilink' | 'footnote';
|
|
1033
|
+
original: number;
|
|
1034
|
+
result: number;
|
|
1035
|
+
/** A human sentence such as "2 links in the original, 1 in the result". */
|
|
1036
|
+
message: string;
|
|
1037
|
+
}
|
|
1038
|
+
interface StructureDiff {
|
|
1039
|
+
/** True when nothing differs. */
|
|
1040
|
+
ok: boolean;
|
|
1041
|
+
mismatches: StructureMismatch[];
|
|
1042
|
+
/** `mismatches[].message`, ready to show. */
|
|
1043
|
+
messages: string[];
|
|
1044
|
+
}
|
|
1045
|
+
declare function countMarkdownStructure(markdown: string): StructureCounts;
|
|
1046
|
+
/**
|
|
1047
|
+
* Compare the structure of a rewritten passage with the original. Zero
|
|
1048
|
+
* mismatches means every counted piece survived; a mismatch is a sentence for
|
|
1049
|
+
* the warning line, not an error.
|
|
1050
|
+
*/
|
|
1051
|
+
declare function checkMarkdownStructure(original: string, result: string): StructureDiff;
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Lenient JSON for local models (D6). Many local models have no JSON mode, so
|
|
1055
|
+
* a reply can arrive as clean JSON, inside a code fence, after a sentence of
|
|
1056
|
+
* prose, behind a `<think>` block, with a trailing comma or with single
|
|
1057
|
+
* quotes. `parseLenientJson` takes all of those; plain text is `ok: false`
|
|
1058
|
+
* with a message the repair turn can quote. Pure, no DOM.
|
|
1059
|
+
*/
|
|
1060
|
+
|
|
1061
|
+
type LenientJsonResult<T = unknown> = {
|
|
1062
|
+
ok: true;
|
|
1063
|
+
value: T;
|
|
1064
|
+
} | {
|
|
1065
|
+
ok: false;
|
|
1066
|
+
error: string;
|
|
1067
|
+
};
|
|
1068
|
+
/**
|
|
1069
|
+
* Strip `<think>`, look inside code fences, take the first balanced `{…}` or
|
|
1070
|
+
* `[…]` that parses (after one repair pass for the common slips) and return
|
|
1071
|
+
* it. A reply with no JSON, or one cut off before it closed, is `ok: false`.
|
|
1072
|
+
* `T` is only the caller's claim about the shape; validate before trusting it.
|
|
1073
|
+
*/
|
|
1074
|
+
declare function parseLenientJson<T = unknown>(raw: string): LenientJsonResult<T>;
|
|
1075
|
+
/**
|
|
1076
|
+
* The one-shot repair turn: append these two messages to the original ones
|
|
1077
|
+
* and ask again. The failed reply goes back as the assistant turn (reasoning
|
|
1078
|
+
* removed, capped), then the parse error.
|
|
1079
|
+
*/
|
|
1080
|
+
declare function jsonRepairMessages(previousRaw: string, error: string): AiMessage[];
|
|
1081
|
+
/**
|
|
1082
|
+
* The prompt line that fixes the reply shape: a one-line example object.
|
|
1083
|
+
* Pass the example as a value (serialised on one line) or as a string.
|
|
1084
|
+
*/
|
|
1085
|
+
declare function JSON_SHAPE_HINT(example: unknown): string;
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Fit a markdown document to a model's context window by whole sections, and
|
|
1089
|
+
* say how much of it fit (never a silent cut). Used by `/summary`, `/title`
|
|
1090
|
+
* and Properties suggestions. Pure, no DOM.
|
|
1091
|
+
*
|
|
1092
|
+
* The kept text is always a prefix of the document: the frontmatter and the
|
|
1093
|
+
* opening sections, ending on a heading boundary. If even the first section
|
|
1094
|
+
* is too big it is cut on a blank line between blocks. A fenced code block is
|
|
1095
|
+
* never split, so a fence that opens inside the kept text also closes in it.
|
|
1096
|
+
*/
|
|
1097
|
+
/** Characters per token the budget assumes (a little pessimistic for English). */
|
|
1098
|
+
declare const CHARS_PER_TOKEN = 3.5;
|
|
1099
|
+
interface DocumentBudgetOptions {
|
|
1100
|
+
/** The model's context window in tokens. */
|
|
1101
|
+
contextTokens: number;
|
|
1102
|
+
/** Tokens the answer may use. */
|
|
1103
|
+
outputTokens: number;
|
|
1104
|
+
/** Tokens for the system prompt, instructions and slack. */
|
|
1105
|
+
reserveTokens: number;
|
|
1106
|
+
}
|
|
1107
|
+
interface BudgetedDocument {
|
|
1108
|
+
/** The markdown to send: frontmatter plus the leading sections that fit. */
|
|
1109
|
+
text: string;
|
|
1110
|
+
/** Share of the document in `text`, 0 to 1 (1 when nothing was cut). */
|
|
1111
|
+
coverage: number;
|
|
1112
|
+
truncated: boolean;
|
|
1113
|
+
/** Sections in `text` (a partly kept first section counts as one). */
|
|
1114
|
+
sectionsIncluded: number;
|
|
1115
|
+
sectionsTotal: number;
|
|
1116
|
+
}
|
|
1117
|
+
interface HeadingOutlineEntry {
|
|
1118
|
+
/** 1 to 6. */
|
|
1119
|
+
level: number;
|
|
1120
|
+
text: string;
|
|
1121
|
+
}
|
|
1122
|
+
interface HeadingWithOffsets extends HeadingOutlineEntry {
|
|
1123
|
+
/** Offset of the heading line's first character. */
|
|
1124
|
+
start: number;
|
|
1125
|
+
/** Offset just past the heading line's end of line (the next line's start, or the text length). */
|
|
1126
|
+
end: number;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Heading levels, texts and line offsets, skipping frontmatter and fenced code.
|
|
1130
|
+
* `limit` stops after that many headings (the documentation index keeps 1000).
|
|
1131
|
+
*/
|
|
1132
|
+
declare function headingsWithOffsets(markdown: string, limit?: number): HeadingWithOffsets[];
|
|
1133
|
+
/** Heading levels and texts, skipping frontmatter and fenced code. `limit` as in `headingsWithOffsets`. */
|
|
1134
|
+
declare function headingsOutline(markdown: string, limit?: number): HeadingOutlineEntry[];
|
|
1135
|
+
/** Offset just past the leading frontmatter block (0 when there is none). */
|
|
1136
|
+
declare function frontmatterEndOffset(markdown: string): number;
|
|
1137
|
+
/**
|
|
1138
|
+
* Keep the frontmatter and as many whole leading sections as fit in
|
|
1139
|
+
* `contextTokens - outputTokens - reserveTokens` (at `CHARS_PER_TOKEN`
|
|
1140
|
+
* characters a token). A first section bigger than the budget is cut on a
|
|
1141
|
+
* block boundary instead, and the first block is kept whatever the budget.
|
|
1142
|
+
*/
|
|
1143
|
+
declare function budgetDocument(markdown: string, opts: DocumentBudgetOptions): BudgetedDocument;
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Summary prompts (the `summary` feature: `/summary` and `/outline`). Pure:
|
|
1147
|
+
* builds the chat messages from the budgeted document and tidies the reply
|
|
1148
|
+
* before it is previewed and inserted.
|
|
1149
|
+
*
|
|
1150
|
+
* TL;DR is wrapped in a `[!NOTE]` callout whose first line is **Summary**
|
|
1151
|
+
* (D11: the GFM five are the only kinds that round-trip on GitHub). Outline
|
|
1152
|
+
* stays a plain nested list.
|
|
1153
|
+
*/
|
|
1154
|
+
|
|
1155
|
+
type AiSummaryKind = 'tldr' | 'outline';
|
|
1156
|
+
interface SummaryPromptInput {
|
|
1157
|
+
/** The document as markdown, already through `budgetDocument`. */
|
|
1158
|
+
document: string;
|
|
1159
|
+
kind: AiSummaryKind;
|
|
1160
|
+
/** Share of the document included, 0 to 1 (below 1 means it was cut). */
|
|
1161
|
+
coverage: number;
|
|
1162
|
+
}
|
|
1163
|
+
/** A document shorter than this gets a one-paragraph TL;DR instead of bullets. */
|
|
1164
|
+
declare const SUMMARY_SHORT_DOCUMENT_CHARS = 1200;
|
|
1165
|
+
declare const SUMMARY_SYSTEM_PROMPT: string;
|
|
1166
|
+
declare const OUTLINE_SYSTEM_PROMPT: string;
|
|
1167
|
+
declare function coveragePercent(coverage: number): number;
|
|
1168
|
+
declare function buildSummaryMessages(input: SummaryPromptInput): AiMessage[];
|
|
1169
|
+
/** Output budget: scales with the input, within a range a small model handles. */
|
|
1170
|
+
declare function summaryMaxTokens(kind: AiSummaryKind, inputChars: number): number;
|
|
1171
|
+
/** The text to show while it streams (reasoning hidden). */
|
|
1172
|
+
declare function summaryVisibleText(raw: string): string;
|
|
1173
|
+
/**
|
|
1174
|
+
* The preview once the stream ends: the body `finalizeSummary` will insert,
|
|
1175
|
+
* without the callout wrapper and the coverage line (the popover shows those
|
|
1176
|
+
* apart), so the cut fragment and lead-in are gone before Insert.
|
|
1177
|
+
*/
|
|
1178
|
+
declare function summaryPreviewText(raw: string, kind: AiSummaryKind, cutOff?: boolean): string;
|
|
1179
|
+
/**
|
|
1180
|
+
* The markdown to insert. TL;DR becomes a `[!NOTE]` callout headed
|
|
1181
|
+
* **Summary**; Outline stays the list. When the document was cut
|
|
1182
|
+
* (`coverage` below 1) a "Covers the first N% of the document." line closes
|
|
1183
|
+
* it. An empty reply stays empty so the caller can report a bad output.
|
|
1184
|
+
* `cutOff` (the stream stopped at the length limit) drops a trailing
|
|
1185
|
+
* fragment and ends the text with an ellipsis.
|
|
1186
|
+
*/
|
|
1187
|
+
declare function finalizeSummary(raw: string, kind: AiSummaryKind, coverage?: number, cutOff?: boolean): string;
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Title suggestions (`/title` and "Suggest titles…", the `title` feature).
|
|
1191
|
+
* Pure: the prompt asks for a small JSON object, `parseTitles` validates it.
|
|
1192
|
+
* The document is the budgeted prefix plus the full headings list, so a long
|
|
1193
|
+
* document is still judged as a whole.
|
|
1194
|
+
*/
|
|
1195
|
+
|
|
1196
|
+
declare const TITLE_MAX_CHARS = 120;
|
|
1197
|
+
/** Longest headings list sent, so a huge outline cannot eat the budget. */
|
|
1198
|
+
declare const TITLE_OUTLINE_MAX_ITEMS = 60;
|
|
1199
|
+
interface TitlePromptInput {
|
|
1200
|
+
/** The document as markdown, already through `budgetDocument`. */
|
|
1201
|
+
document: string;
|
|
1202
|
+
/** All headings of the full document (not only the budgeted part). */
|
|
1203
|
+
outline: readonly HeadingOutlineEntry[];
|
|
1204
|
+
/** The current title, so the model does not hand it back. */
|
|
1205
|
+
existingTitle?: string;
|
|
1206
|
+
count?: number;
|
|
1207
|
+
}
|
|
1208
|
+
declare const TITLE_SYSTEM_PROMPT: string;
|
|
1209
|
+
/** The headings as an indented list, capped. Shared by the title and properties prompts. */
|
|
1210
|
+
declare function renderHeadingOutline(outline: readonly HeadingOutlineEntry[], max?: number): string;
|
|
1211
|
+
declare function buildTitleMessages(input: TitlePromptInput): AiMessage[];
|
|
1212
|
+
/** Output budget: a few short strings. */
|
|
1213
|
+
declare function titleMaxTokens(count?: number): number;
|
|
1214
|
+
type ParsedTitles = {
|
|
1215
|
+
ok: true;
|
|
1216
|
+
titles: string[];
|
|
1217
|
+
} | {
|
|
1218
|
+
ok: false;
|
|
1219
|
+
error: string;
|
|
1220
|
+
};
|
|
1221
|
+
/**
|
|
1222
|
+
* The titles in a reply: `{ "titles": [...] }` (a bare array or a single
|
|
1223
|
+
* `title` string is accepted too), trimmed to 120 characters, de-duplicated
|
|
1224
|
+
* case-insensitively, minus the existing title. No usable title is a failure
|
|
1225
|
+
* so the caller can repair or report `badOutput`.
|
|
1226
|
+
*/
|
|
1227
|
+
declare function parseTitles(raw: string, existingTitle?: string): ParsedTitles;
|
|
1228
|
+
|
|
1229
|
+
interface ParsedEntry {
|
|
1230
|
+
key: string;
|
|
1231
|
+
/** Scalar string, or array of strings (for `tags: [a, b]`-style). */
|
|
1232
|
+
value: string | string[];
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Properties suggestions (the `frontmatter` feature: "Suggest" in the
|
|
1237
|
+
* sidebar's Properties header). Pure: the prompt asks for a title, a
|
|
1238
|
+
* description and tags as JSON; `parsePropertySuggestions` validates the
|
|
1239
|
+
* reply and normalises tags through the shared tag syntax, so what reaches the
|
|
1240
|
+
* UI is already safe to apply.
|
|
1241
|
+
*/
|
|
1242
|
+
|
|
1243
|
+
/** A frontmatter property as the sidebar holds it. */
|
|
1244
|
+
type AiFrontmatterEntry = ParsedEntry;
|
|
1245
|
+
declare const PROPERTY_MAX_TAGS = 8;
|
|
1246
|
+
declare const PROPERTY_DESCRIPTION_MAX_CHARS = 200;
|
|
1247
|
+
declare const PROPERTY_TITLE_MAX_CHARS = 120;
|
|
1248
|
+
/** Vocabulary tags listed in the prompt at most. */
|
|
1249
|
+
declare const PROPERTY_VOCABULARY_MAX = 80;
|
|
1250
|
+
interface PropertiesPromptInput {
|
|
1251
|
+
/** The document as markdown, already through `budgetDocument`. */
|
|
1252
|
+
document: string;
|
|
1253
|
+
/** All headings of the full document. */
|
|
1254
|
+
outline: readonly HeadingOutlineEntry[];
|
|
1255
|
+
/** The properties the document already has. */
|
|
1256
|
+
existing: readonly AiFrontmatterEntry[];
|
|
1257
|
+
/** Tags used elsewhere in the workspace, most used first. */
|
|
1258
|
+
vocabulary: readonly string[];
|
|
1259
|
+
}
|
|
1260
|
+
declare const PROPERTIES_SYSTEM_PROMPT: string;
|
|
1261
|
+
/** The tags the entries already carry, normalised (`tags:` or `tag:`, array or comma list). */
|
|
1262
|
+
declare function existingTagsOf(existing: readonly AiFrontmatterEntry[]): string[];
|
|
1263
|
+
declare function buildPropertiesMessages(input: PropertiesPromptInput): AiMessage[];
|
|
1264
|
+
/** Output budget: three short fields. */
|
|
1265
|
+
declare function propertiesMaxTokens(): number;
|
|
1266
|
+
interface SuggestedTag {
|
|
1267
|
+
/** Normalised: lowercase, no `#`. */
|
|
1268
|
+
tag: string;
|
|
1269
|
+
/** Already used somewhere in the workspace (it is in the vocabulary). */
|
|
1270
|
+
known: boolean;
|
|
1271
|
+
}
|
|
1272
|
+
interface PropertySuggestions {
|
|
1273
|
+
title?: string;
|
|
1274
|
+
description?: string;
|
|
1275
|
+
tags: SuggestedTag[];
|
|
1276
|
+
}
|
|
1277
|
+
type ParsedPropertySuggestions = {
|
|
1278
|
+
ok: true;
|
|
1279
|
+
suggestions: PropertySuggestions;
|
|
1280
|
+
} | {
|
|
1281
|
+
ok: false;
|
|
1282
|
+
error: string;
|
|
1283
|
+
};
|
|
1284
|
+
/**
|
|
1285
|
+
* Validate a reply: title and description trimmed to length and dropped when
|
|
1286
|
+
* empty or equal to what the document has, tags normalised, marked `known`
|
|
1287
|
+
* from the vocabulary, minus tags already present, de-duplicated and capped at
|
|
1288
|
+
* 8. A reply with no title, description or tag at all is a failure; one whose
|
|
1289
|
+
* suggestions are all already present succeeds with nothing left in it.
|
|
1290
|
+
*/
|
|
1291
|
+
declare function parsePropertySuggestions(raw: string, existing: readonly AiFrontmatterEntry[], vocabulary: readonly string[]): ParsedPropertySuggestions;
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* Inline suggestions ("ghost text", the `ghostText` feature). Pure: a chat
|
|
1295
|
+
* "continue" prompt sent to every model (decision D4; the wire has no FIM
|
|
1296
|
+
* path). The text before the caret is the user turn; the text after it rides
|
|
1297
|
+
* in an `<after>` tag so the model stops where the existing text resumes.
|
|
1298
|
+
*/
|
|
1299
|
+
|
|
1300
|
+
/** Which kind of text block the caret is in; steers the continuation's shape. */
|
|
1301
|
+
type GhostBlockKind = 'paragraph' | 'heading' | 'listItem' | 'blockquote';
|
|
1302
|
+
/** Default and bounds for the output budget (tokens). */
|
|
1303
|
+
declare const CONTINUE_DEFAULT_MAX_TOKENS = 48;
|
|
1304
|
+
declare const CONTINUE_MIN_MAX_TOKENS = 8;
|
|
1305
|
+
declare const CONTINUE_MAX_MAX_TOKENS = 256;
|
|
1306
|
+
declare const CONTINUE_SYSTEM_PROMPT: string;
|
|
1307
|
+
interface ContinuePromptInput {
|
|
1308
|
+
/** The document text before the caret (see `ghostContext`). */
|
|
1309
|
+
before: string;
|
|
1310
|
+
/** The document text after the caret, possibly empty. */
|
|
1311
|
+
after: string;
|
|
1312
|
+
blockKind?: GhostBlockKind;
|
|
1313
|
+
}
|
|
1314
|
+
declare function buildContinueMessages(input: ContinuePromptInput): AiMessage[];
|
|
1315
|
+
/**
|
|
1316
|
+
* Output budget for a request: the `features.ghostText.maxTokens` setting
|
|
1317
|
+
* (default 48), rounded and clamped to 8 to 256. A bad value gets the default.
|
|
1318
|
+
*/
|
|
1319
|
+
declare function continueMaxTokens(setting?: number | null): number;
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* The text a ghost-text request sends (decision D3): the text around the
|
|
1323
|
+
* cursor. Pure: given the document's markdown and the caret's offset in it,
|
|
1324
|
+
* return the last `GHOST_BEFORE_CHARS` characters before the caret verbatim
|
|
1325
|
+
* (a few paragraphs), a capped slice of the text after it, and, when the
|
|
1326
|
+
* document runs longer than that tail, a note listing the headings of
|
|
1327
|
+
* everything earlier so the model still knows where in the document it is.
|
|
1328
|
+
* The earlier text itself is never sent, however large the context window.
|
|
1329
|
+
* A small window shrinks the tail (never below 200 characters). Frontmatter
|
|
1330
|
+
* is never part of `before`.
|
|
1331
|
+
*/
|
|
1332
|
+
/** Characters before the caret that are always sent verbatim. */
|
|
1333
|
+
declare const GHOST_BEFORE_CHARS = 2400;
|
|
1334
|
+
/** Characters after the caret that are sent as the suffix. */
|
|
1335
|
+
declare const GHOST_AFTER_CHARS = 600;
|
|
1336
|
+
/** Tokens set aside for the system prompt, the tags and slack. */
|
|
1337
|
+
declare const GHOST_RESERVE_TOKENS = 400;
|
|
1338
|
+
/** Most headings listed for the sections left out. */
|
|
1339
|
+
declare const GHOST_OUTLINE_MAX_ITEMS = 40;
|
|
1340
|
+
/** Characters set aside for the list of headings left out. */
|
|
1341
|
+
declare const GHOST_OUTLINE_MAX_CHARS = 1000;
|
|
1342
|
+
/** Window assumed when a route reports none. */
|
|
1343
|
+
declare const GHOST_DEFAULT_CONTEXT_TOKENS = 4096;
|
|
1344
|
+
interface GhostContext {
|
|
1345
|
+
before: string;
|
|
1346
|
+
after: string;
|
|
1347
|
+
}
|
|
1348
|
+
interface GhostContextOptions {
|
|
1349
|
+
/** Tokens the answer may use (the `maxTokens` setting). Default 48. */
|
|
1350
|
+
outputTokens?: number;
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* `before` and `after` for a caret at `caretOffset` in `markdown`. A caret
|
|
1354
|
+
* inside the frontmatter yields nothing (both empty), since there is nothing
|
|
1355
|
+
* to continue there. An `after` that is only whitespace is returned empty.
|
|
1356
|
+
*/
|
|
1357
|
+
declare function ghostContext(markdown: string, caretOffset: number, contextTokens: number, opts?: GhostContextOptions): GhostContext;
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* Clean up a ghost-text suggestion as it streams in (decisions D5 and D6).
|
|
1361
|
+
* Pure string work, no DOM. The controller calls `trimGhostSuggestion` on
|
|
1362
|
+
* the accumulated raw text after every token, so every rule here must give a
|
|
1363
|
+
* stable answer for a partial string.
|
|
1364
|
+
*/
|
|
1365
|
+
/** A suggestion longer than this is cut at the end of its next sentence. */
|
|
1366
|
+
declare const GHOST_SENTENCE_CUT_CHARS = 120;
|
|
1367
|
+
/** How many characters before the caret a suggestion may not repeat. */
|
|
1368
|
+
declare const GHOST_REPEAT_CHARS = 20;
|
|
1369
|
+
interface GhostTrimOptions {
|
|
1370
|
+
/** The caret is at the end of its text block (nothing after it in the block). */
|
|
1371
|
+
atBlockEnd: boolean;
|
|
1372
|
+
/** The text before the caret (only its tail is used). */
|
|
1373
|
+
before: string;
|
|
1374
|
+
}
|
|
1375
|
+
interface GhostTrimResult {
|
|
1376
|
+
/** The suggestion to show; empty means show nothing. */
|
|
1377
|
+
text: string;
|
|
1378
|
+
/** A stop rule fired (blank line, newline, sentence cut) or the suggestion was discarded: more tokens cannot change it, so the request can abort. */
|
|
1379
|
+
stopped: boolean;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Apply the stop rules to `raw`: strip a stray code fence, cut at the first
|
|
1383
|
+
* blank line (and at the first newline when the caret is not at the end of
|
|
1384
|
+
* its block), cut after the first sentence that ends past 120 characters,
|
|
1385
|
+
* drop trailing whitespace, and discard a suggestion that is empty or starts
|
|
1386
|
+
* by repeating the last 20 characters before the caret. A space at the start
|
|
1387
|
+
* is dropped when the text before the caret already ends in whitespace.
|
|
1388
|
+
*/
|
|
1389
|
+
declare function trimGhostSuggestionDetailed(raw: string, opts: GhostTrimOptions): GhostTrimResult;
|
|
1390
|
+
/** The suggestion to show for the raw model output so far. Empty means nothing. */
|
|
1391
|
+
declare function trimGhostSuggestion(raw: string, opts: GhostTrimOptions): string;
|
|
1392
|
+
/**
|
|
1393
|
+
* The first word of `text` for "accept one word": its leading whitespace,
|
|
1394
|
+
* the word, and the one space or tab after it. The whole text when it has
|
|
1395
|
+
* no second word.
|
|
1396
|
+
*/
|
|
1397
|
+
declare function ghostAcceptWord(text: string): string;
|
|
1398
|
+
/**
|
|
1399
|
+
* The user typed `typed` while `text` was showing: the rest of the
|
|
1400
|
+
* suggestion when `typed` is exactly its start (case-sensitive), else null
|
|
1401
|
+
* (dismiss). An empty result means the whole suggestion was typed.
|
|
1402
|
+
*/
|
|
1403
|
+
declare function ghostPrefixMatch(text: string, typed: string): string | null;
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* Every cap and clamp of the documentation-base feature, in one place (S9).
|
|
1407
|
+
* Hosts and the loop import these; nothing else hardcodes a number. The hop
|
|
1408
|
+
* and share limits are M1's (`AI_DOCS_*` in `../types`), re-exported here so a
|
|
1409
|
+
* caller needs one import.
|
|
1410
|
+
*/
|
|
1411
|
+
|
|
1412
|
+
/** Markdown files indexed per base (decision 13). The walk stops at this many. */
|
|
1413
|
+
declare const DOCS_MAX_FILES = 5000;
|
|
1414
|
+
/** Bytes per file; a bigger file is skipped and counted in health. */
|
|
1415
|
+
declare const DOCS_MAX_FILE_BYTES: number;
|
|
1416
|
+
/** Bytes per section sent to a model (the host cuts at this and sets `truncated`). */
|
|
1417
|
+
declare const DOCS_MAX_SECTION_BYTES: number;
|
|
1418
|
+
/** Links in one hop-1 list. */
|
|
1419
|
+
declare const DOCS_MAX_LINKS = 200;
|
|
1420
|
+
/** Pages in the in-memory map used when a root has no home document. */
|
|
1421
|
+
declare const DOCS_MAX_MAP_ENTRIES = 400;
|
|
1422
|
+
/** Sections the keyword fallback returns. */
|
|
1423
|
+
declare const DOCS_KEYWORD_TOP = 20;
|
|
1424
|
+
/** Terms the keyword fallback uses from one query. */
|
|
1425
|
+
declare const DOCS_KEYWORD_MAX_TERMS = 12;
|
|
1426
|
+
/** Headings and links kept per page in the index (defence against one huge file). */
|
|
1427
|
+
declare const DOCS_MAX_HEADINGS_PER_PAGE = 1000;
|
|
1428
|
+
declare const DOCS_MAX_LINKS_PER_PAGE = 1000;
|
|
1429
|
+
/** Folders read per base while walking; the walk stops at this many and says so in health (S9). */
|
|
1430
|
+
declare const DOCS_MAX_DIRS = 20000;
|
|
1431
|
+
/** Heading text is cut to this many code points in the index and in sections (same rule on both hosts). */
|
|
1432
|
+
declare const DOCS_MAX_HEADING_CHARS = 200;
|
|
1433
|
+
/** A link target is cut to this many code points in the index. */
|
|
1434
|
+
declare const DOCS_MAX_LINK_TARGET_CHARS = 500;
|
|
1435
|
+
/**
|
|
1436
|
+
* How far (UTF-16 units) the link scanner looks for the end of a `<...>`
|
|
1437
|
+
* destination or a quoted title. A link whose destination or title does not
|
|
1438
|
+
* end within it is not a link. Link text is bounded by the same 300 as before.
|
|
1439
|
+
*/
|
|
1440
|
+
declare const DOCS_LINK_SCAN_UNITS = 1000;
|
|
1441
|
+
/** Link text between the brackets, in UTF-16 units (longer is not a link). */
|
|
1442
|
+
declare const DOCS_LINK_TEXT_UNITS = 300;
|
|
1443
|
+
/** Milliseconds one base's walk and index may take; past it the build ends and health says so (S9). */
|
|
1444
|
+
declare const DOCS_BUILD_BUDGET_MS = 30000;
|
|
1445
|
+
/** Candidate pages looked at while counting broken wikilinks, per base; past it the count stops (a lower bound). */
|
|
1446
|
+
declare const DOCS_MAX_LINK_WORK = 5000000;
|
|
1447
|
+
/** Ids taken from one navigation reply; the rest are dropped and counted. Model output is hostile. */
|
|
1448
|
+
declare const DOCS_MAX_READ_IDS_PER_HOP = 20;
|
|
1449
|
+
/** Characters of index text in one navigation prompt, whatever the route's context (several roots of 400 map entries add up). */
|
|
1450
|
+
declare const DOCS_MAX_NAV_CHARS = 96000;
|
|
1451
|
+
/** Longest id the loop looks up; anything longer is dropped unread. */
|
|
1452
|
+
declare const DOCS_MAX_ID_CHARS = 2048;
|
|
1453
|
+
/** Ids an Activity entry lists per field, each clipped; the count stays exact. */
|
|
1454
|
+
declare const DOCS_MAX_ACTIVITY_IDS = 100;
|
|
1455
|
+
declare const DOCS_MAX_ACTIVITY_ID_CHARS = 200;
|
|
1456
|
+
|
|
1457
|
+
/** Navigation calls before the answer, 1 to 5; anything not a number is the default (3). */
|
|
1458
|
+
declare function clampDocsHops(value: unknown): number;
|
|
1459
|
+
/** Percent of the route's context the answer's sections may take, 30 to 80; default 60. */
|
|
1460
|
+
declare function clampDocsSourceShare(value: unknown): number;
|
|
1461
|
+
/**
|
|
1462
|
+
* Cut a section's text to `DOCS_MAX_SECTION_BYTES` UTF-8 bytes on a code point
|
|
1463
|
+
* boundary. Scan (`secrets.ts`) the uncapped text first when you can, so a key
|
|
1464
|
+
* is never cut in half and slips through.
|
|
1465
|
+
*/
|
|
1466
|
+
declare function capSectionText(text: string, maxBytes?: number): {
|
|
1467
|
+
text: string;
|
|
1468
|
+
truncated: boolean;
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* Small text helpers the documentation-base modules share. Every rule here is
|
|
1473
|
+
* written to be ported to Rust exactly (Desktop), so none of them leans on
|
|
1474
|
+
* locale behaviour or on a Unicode table.
|
|
1475
|
+
*/
|
|
1476
|
+
/**
|
|
1477
|
+
* Case folding used by ids, slugs and keyword ranking: ASCII `A` to `Z`
|
|
1478
|
+
* become `a` to `z`, every other code point is left alone. (No Unicode
|
|
1479
|
+
* lowercasing: `É` stays `É`, and `İ` or the Kelvin sign are not touched, so
|
|
1480
|
+
* JavaScript and Rust agree without a table.)
|
|
1481
|
+
*/
|
|
1482
|
+
declare function foldAscii(text: string): string;
|
|
1483
|
+
/**
|
|
1484
|
+
* Compare two strings by Unicode code point (the order UTF-8 bytes give, so
|
|
1485
|
+
* Rust's `str::cmp` agrees). JavaScript's `<` compares UTF-16 units, which
|
|
1486
|
+
* differs for characters above U+FFFF. Returns -1, 0 or 1.
|
|
1487
|
+
*/
|
|
1488
|
+
declare function cmpCodePoints(a: string, b: string): number;
|
|
1489
|
+
/**
|
|
1490
|
+
* The first `max` code points of `text` (the whole text when it has no more).
|
|
1491
|
+
* A surrogate pair counts as one and is never cut. Rust takes `chars().take(max)`.
|
|
1492
|
+
*/
|
|
1493
|
+
declare function clipCodePoints(text: string, max: number): string;
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Documentation-base index: root labels, page ids, heading slugs and the
|
|
1497
|
+
* builder hosts call. Pure, no DOM, no network.
|
|
1498
|
+
*
|
|
1499
|
+
* THE ID CONTRACT (A10). The VS Code host (TypeScript) and Desktop (Rust)
|
|
1500
|
+
* must produce byte-identical labels, ids and slugs; `test/ai-fixtures/docs/`
|
|
1501
|
+
* holds the cases both run. Nothing here uses a regex feature or Unicode table
|
|
1502
|
+
* Rust's standard library lacks.
|
|
1503
|
+
*
|
|
1504
|
+
* ROOT LABEL (`docsRootLabels`). A base has one or more root folders. Each
|
|
1505
|
+
* gets a label, the first segment of every id under it.
|
|
1506
|
+
* 1. Take the root path, split it on `/` and `\`, drop empty segments, take
|
|
1507
|
+
* the last one (the folder name). A path with no segment gives `root`.
|
|
1508
|
+
* 2. Replace `#` and every control character (below U+0020, and U+007F)
|
|
1509
|
+
* with `_`. A segment that is `.` or `..` becomes `root`.
|
|
1510
|
+
* 3. Make labels unique within the base, in configured order: the first
|
|
1511
|
+
* root keeps its label; a later root whose label is already taken gets
|
|
1512
|
+
* `-2`, `-3`, ... appended (the smallest suffix whose result is unused).
|
|
1513
|
+
* So `/a/docs` and `/b/docs` are `docs` and `docs-2`; a third `docs`
|
|
1514
|
+
* folder is `docs-3`. Comparison is exact (case-sensitive).
|
|
1515
|
+
* Labels are only unique inside one base. Ids from two bases can be equal, so
|
|
1516
|
+
* the loop always pairs an id with its base id.
|
|
1517
|
+
*
|
|
1518
|
+
* PAGE ID. `<root label>/<rel path>`, the path relative to the root with `/`
|
|
1519
|
+
* separators and exactly as the file system spells it (case kept), e.g.
|
|
1520
|
+
* `docs/guides/setup.md`. A rel path never starts with `/`, is never empty
|
|
1521
|
+
* and has no `.` or `..` segment (such a page is dropped from the index).
|
|
1522
|
+
*
|
|
1523
|
+
* SECTION ID. `<page id>#<slug>`, one per heading `headingsOutline` reports
|
|
1524
|
+
* (ATX headings outside frontmatter and fenced code, with non-empty text).
|
|
1525
|
+
* A bare page id means the whole page. File names can contain `#`, so
|
|
1526
|
+
* `parseDocsId` looks a full id up as a page first and only then splits at
|
|
1527
|
+
* the LAST `#` (slugs never contain `#`).
|
|
1528
|
+
*
|
|
1529
|
+
* HEADING SLUG (`docsSlug`, `docsSlugs`). From the heading text as
|
|
1530
|
+
* `headingsOutline` returns it (trimmed, closing `#`s removed):
|
|
1531
|
+
* 1. Walk the code points. ASCII `A-Z` become `a-z`. `a-z`, `0-9`, `_` and
|
|
1532
|
+
* `-` are kept. Whitespace (ASCII space, tab, LF, VT, FF, CR, plus
|
|
1533
|
+
* U+0085, U+00A0, U+1680, U+2000-U+200A, U+2028, U+2029, U+202F, U+205F,
|
|
1534
|
+
* U+3000) becomes `-`, one `-` per whitespace code point (not collapsed,
|
|
1535
|
+
* as GitHub does). Any other ASCII character is dropped. Every non-ASCII
|
|
1536
|
+
* code point is kept unchanged (no lowercasing), so non-Latin headings
|
|
1537
|
+
* still get a distinct slug.
|
|
1538
|
+
* 2. An empty result becomes `section`.
|
|
1539
|
+
* 3. Duplicates on one page, in document order: the first keeps its slug;
|
|
1540
|
+
* a later heading with a taken slug gets `-1`, `-2`, ... appended (the
|
|
1541
|
+
* smallest suffix whose result is unused, so `Setup`, `Setup`, `Setup 1`
|
|
1542
|
+
* give `setup`, `setup-1`, `setup-1-1`).
|
|
1543
|
+
* This equals GitHub's anchor for ASCII headings without odd collisions. Link
|
|
1544
|
+
* fragments are also matched against GitHub's anchor (`links.ts`).
|
|
1545
|
+
*
|
|
1546
|
+
* LIMITS (S9, the same on both hosts; `caps.ts`). A page keeps its first 1000
|
|
1547
|
+
* headings, and a heading's text is cut to its first 200 code points BEFORE
|
|
1548
|
+
* the slug is made, in the index and in `splitDocsSections` alike, so ids
|
|
1549
|
+
* agree. A link target is cut to its first 500 code points (`extractDocsLinks`).
|
|
1550
|
+
* Duplicate suffixes are handed out from a counter per base slug (same result
|
|
1551
|
+
* as "smallest unused suffix", in linear time).
|
|
1552
|
+
*
|
|
1553
|
+
* ORDER. `buildDocsIndex` sorts pages by root order, then rel path by code
|
|
1554
|
+
* point, so the same files give the same index on both hosts.
|
|
1555
|
+
*/
|
|
1556
|
+
|
|
1557
|
+
/** One unique label per root, in the order given (see the header for the rule). */
|
|
1558
|
+
declare function docsRootLabels(roots: readonly string[]): string[];
|
|
1559
|
+
/** The base slug of one heading text, before duplicate suffixes. */
|
|
1560
|
+
declare function docsSlug(text: string): string;
|
|
1561
|
+
/** Unique slugs for a page's heading texts, in document order. */
|
|
1562
|
+
declare function docsSlugs(texts: readonly string[]): string[];
|
|
1563
|
+
/** Heading text as the index and the sections keep it: at most `DOCS_MAX_HEADING_CHARS` code points. */
|
|
1564
|
+
declare function clipDocsHeading(text: string): string;
|
|
1565
|
+
/** GitHub's anchor for each heading text, with its `-1`, `-2` suffixes (used to match link fragments). */
|
|
1566
|
+
declare function githubAnchors(texts: readonly string[]): string[];
|
|
1567
|
+
/** `<root label>/<rel path>`. */
|
|
1568
|
+
declare function docsPageId(rootLabel: string, relPath: string): string;
|
|
1569
|
+
/** `<page id>#<slug>`. */
|
|
1570
|
+
declare function docsSectionId(pageId: string, slug: string): string;
|
|
1571
|
+
/** The root label of a page or section id: everything before the first `/`. */
|
|
1572
|
+
declare function docsIdRoot(id: string): string;
|
|
1573
|
+
/**
|
|
1574
|
+
* Split an id the model or host named into its page id and optional slug.
|
|
1575
|
+
* `hasPage` says whether a full string is a page in the host's own index; the
|
|
1576
|
+
* whole id is tried first (a file name may contain `#`), then the part before
|
|
1577
|
+
* the last `#`. Returns null when neither is a known page: never build a path
|
|
1578
|
+
* from an id that is not in the index (S1).
|
|
1579
|
+
*/
|
|
1580
|
+
declare function parseDocsId(id: string, hasPage: (pageId: string) => boolean): {
|
|
1581
|
+
pageId: string;
|
|
1582
|
+
slug?: string;
|
|
1583
|
+
} | null;
|
|
1584
|
+
/** True for a rel path a page may have: non-empty, relative, no empty, `.` or `..` segment, no NUL. */
|
|
1585
|
+
declare function isSafeRelPath(relPath: string): boolean;
|
|
1586
|
+
/** The file name without its `.md` / `.markdown` extension (any case). */
|
|
1587
|
+
declare function docsFileStem(relPath: string): string;
|
|
1588
|
+
/** A page as the host extracts it: no id, no slugs, no body. */
|
|
1589
|
+
interface AiDocsPageInput {
|
|
1590
|
+
/** The root's label (`docsRootLabels`). */
|
|
1591
|
+
root: string;
|
|
1592
|
+
/** Path relative to the root, `/` separators. */
|
|
1593
|
+
relPath: string;
|
|
1594
|
+
/** Frontmatter `title`, when there is one. Else the first heading, else the file name. */
|
|
1595
|
+
title?: string;
|
|
1596
|
+
/** From `headingsOutline(markdown)`. */
|
|
1597
|
+
headings: Array<{
|
|
1598
|
+
level: number;
|
|
1599
|
+
text: string;
|
|
1600
|
+
}>;
|
|
1601
|
+
/** Links as written (`AiDocsLink`). */
|
|
1602
|
+
links?: AiDocsLink[];
|
|
1603
|
+
/** File length in characters. */
|
|
1604
|
+
chars: number;
|
|
1605
|
+
}
|
|
1606
|
+
interface AiDocsIndexInput {
|
|
1607
|
+
baseId: string;
|
|
1608
|
+
/** Roots that were found, in configured order, each with its label. */
|
|
1609
|
+
roots: Array<{
|
|
1610
|
+
label: string;
|
|
1611
|
+
}>;
|
|
1612
|
+
/** The base's `homeFile` override, a rel path (applied under every root). */
|
|
1613
|
+
homeFile?: string;
|
|
1614
|
+
pages: readonly AiDocsPageInput[];
|
|
1615
|
+
/** The file cap cut the walk short (the builder also sets it past `DOCS_MAX_FILES`). */
|
|
1616
|
+
truncated?: boolean;
|
|
1617
|
+
oversizeSkipped?: number;
|
|
1618
|
+
/** Unix milliseconds; the builder is pure, so the host supplies the clock. */
|
|
1619
|
+
builtAt?: number;
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Build a base's index from host-extracted pages: ids, unique slugs, titles,
|
|
1623
|
+
* deduped and sorted pages (a page with an unknown root label or an unsafe rel
|
|
1624
|
+
* path is dropped; for a repeated id the first in sorted order wins), the file
|
|
1625
|
+
* cap, and each root's home.
|
|
1626
|
+
*/
|
|
1627
|
+
declare function buildDocsIndex(input: AiDocsIndexInput): AiDocsIndex;
|
|
1628
|
+
/** Pages by id, for the lookups the loop and the link resolver do. */
|
|
1629
|
+
declare function docsPagesById(index: Pick<AiDocsIndex, 'pages'>): Map<string, AiDocsPage>;
|
|
1630
|
+
|
|
1631
|
+
/**
|
|
1632
|
+
* Split a markdown file into the sections the index names. Pure.
|
|
1633
|
+
*
|
|
1634
|
+
* THE SECTION CONTRACT (hosts port this for `readSections` and the keyword
|
|
1635
|
+
* scan; `test/ai-fixtures/docs/sections.json` holds the cases).
|
|
1636
|
+
* - Headings are the ones `headingsOutline` reports: ATX (`#` to `######`,
|
|
1637
|
+
* up to three spaces of indent), outside frontmatter and fenced code,
|
|
1638
|
+
* with non-empty text.
|
|
1639
|
+
* - A heading's section runs from its line to the line before the NEXT
|
|
1640
|
+
* heading of any level (sections do not overlap, so a budget never counts
|
|
1641
|
+
* text twice). The last one runs to the end of the file.
|
|
1642
|
+
* - Text before the first heading is the preamble, section 0, with no
|
|
1643
|
+
* heading. It includes the frontmatter. It exists only when there is
|
|
1644
|
+
* non-whitespace text between the frontmatter and the first heading (or
|
|
1645
|
+
* the file has no heading at all and is not blank). A file that opens
|
|
1646
|
+
* with a heading has no preamble, and its frontmatter belongs to no
|
|
1647
|
+
* section (it is still in a whole-page read).
|
|
1648
|
+
* - Only the first 1000 headings (`DOCS_MAX_HEADINGS_PER_PAGE`) open a section;
|
|
1649
|
+
* a heading after that is ordinary text of the 1000th section. A heading's
|
|
1650
|
+
* text is cut to its first 200 code points (`clipDocsHeading`) before its
|
|
1651
|
+
* slug is made, as in the index.
|
|
1652
|
+
* - A section's id is `<page id>#<slug>` (slugs: `docsSlugs`); the preamble
|
|
1653
|
+
* and a whole-page read are the bare page id.
|
|
1654
|
+
*/
|
|
1655
|
+
interface DocsMarkdownSection {
|
|
1656
|
+
/** Position in the file; the preamble, when there is one, is 0. */
|
|
1657
|
+
index: number;
|
|
1658
|
+
/** Null for the preamble. */
|
|
1659
|
+
heading: {
|
|
1660
|
+
level: number;
|
|
1661
|
+
text: string;
|
|
1662
|
+
slug: string;
|
|
1663
|
+
} | null;
|
|
1664
|
+
/** Offset of the section's first character, and one past its last. */
|
|
1665
|
+
start: number;
|
|
1666
|
+
end: number;
|
|
1667
|
+
/** The section's markdown (`slice(start, end)`). */
|
|
1668
|
+
text: string;
|
|
1669
|
+
}
|
|
1670
|
+
declare function splitDocsSections(markdown: string): DocsMarkdownSection[];
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* Home document per root (decision 3) and the in-memory map used when a root
|
|
1674
|
+
* has none. Pure.
|
|
1675
|
+
*
|
|
1676
|
+
* Precedence, per root: the base's override file (`homeFile`, a rel path),
|
|
1677
|
+
* then `INDEX.md`, then `README.md` at the root's top level, then the map.
|
|
1678
|
+
* Names match with ASCII case folding (`readme.md` counts); when several
|
|
1679
|
+
* pages fold to the same name the exact-case one wins, then the first in
|
|
1680
|
+
* sorted order.
|
|
1681
|
+
*/
|
|
1682
|
+
|
|
1683
|
+
/** The health text for a root with no home document. */
|
|
1684
|
+
declare const DOCS_NO_HOME_TEXT = "No home document";
|
|
1685
|
+
/** Pick the home of each root. `pages` must be the index's (sorted) pages. */
|
|
1686
|
+
declare function chooseDocsHomes(rootLabels: readonly string[], pages: readonly AiDocsPage[], homeFile?: string): AiDocsHome[];
|
|
1687
|
+
/** True when no root of the index has a real home. */
|
|
1688
|
+
declare function docsHasNoHome(homes: readonly AiDocsHome[]): boolean;
|
|
1689
|
+
/** The health line for one root's home: `null` when it has one, else "No home document". */
|
|
1690
|
+
declare function docsHomeHealth(home: AiDocsHome): string | null;
|
|
1691
|
+
interface DocsMapEntry {
|
|
1692
|
+
pageId: string;
|
|
1693
|
+
title: string;
|
|
1694
|
+
/** Texts of the page's level 1 to 3 headings, at most `DOCS_MAP_HEADINGS_PER_PAGE`. */
|
|
1695
|
+
headings: string[];
|
|
1696
|
+
}
|
|
1697
|
+
interface DocsMap {
|
|
1698
|
+
root: string;
|
|
1699
|
+
entries: DocsMapEntry[];
|
|
1700
|
+
/** Pages of the root that did not fit the cap. */
|
|
1701
|
+
omitted: number;
|
|
1702
|
+
}
|
|
1703
|
+
declare const DOCS_MAP_HEADINGS_PER_PAGE = 8;
|
|
1704
|
+
/**
|
|
1705
|
+
* The in-memory map of one root: titles and headings of its pages, at most
|
|
1706
|
+
* `cap` pages (400). When the root has more, the shallowest pages are kept
|
|
1707
|
+
* (fewest folders, then path order) and the entries are listed in path order.
|
|
1708
|
+
* Nothing is written to disk.
|
|
1709
|
+
*/
|
|
1710
|
+
declare function buildDocsMap(index: Pick<AiDocsIndex, 'pages'>, root: string, cap?: number): DocsMap;
|
|
1711
|
+
/** One line per page: `- <page id> | <title> | <heading>; <heading>`; a final line counts what was left out. */
|
|
1712
|
+
declare function renderDocsMap(map: DocsMap): string;
|
|
1713
|
+
|
|
1714
|
+
/**
|
|
1715
|
+
* Resolve the links of a base's pages inside its index (decision 14), mark the
|
|
1716
|
+
* broken ones and produce the hop-1 link list. Pure; the host only extracts
|
|
1717
|
+
* links as written (`AiDocsLink`).
|
|
1718
|
+
*
|
|
1719
|
+
* Rules.
|
|
1720
|
+
* - A link with a scheme (`https:`, `mailto:`, anything `name:` with two or
|
|
1721
|
+
* more letters before the colon) or starting `//` is `external`: not
|
|
1722
|
+
* followable, not counted as broken.
|
|
1723
|
+
* - A relative link resolves from the linking page's folder. `?query` is
|
|
1724
|
+
* dropped, `%XX` decoded (kept raw when malformed), `.` and `..`
|
|
1725
|
+
* normalised. A link that climbs above its own root, or starts with `/`
|
|
1726
|
+
* or a drive letter, is broken (`outside-root`) and never followable.
|
|
1727
|
+
* The index does not know where roots sit on disk, so a link into a
|
|
1728
|
+
* sibling root is broken too, even when that folder is another root of the
|
|
1729
|
+
* base.
|
|
1730
|
+
* - The target must be a page of the index (exact id; if the path has no
|
|
1731
|
+
* `.md` / `.markdown` extension, `.md` is tried). Else broken `not-found`.
|
|
1732
|
+
* - `#fragment` is matched against the page's heading slugs, then GitHub's
|
|
1733
|
+
* anchors. A fragment that matches nothing still resolves to the page
|
|
1734
|
+
* (`fragmentMissing`): the page is real.
|
|
1735
|
+
* - Wikilinks (`[[Name]]`, `[[Name#Heading]]`, `[[folder/Name|alias]]`)
|
|
1736
|
+
* match pages by file stem, ASCII case-folded, anywhere in the base. With
|
|
1737
|
+
* several matches the nearest to the linking page wins (same folder, then
|
|
1738
|
+
* fewest folder hops, then the shorter id, then code point order) and
|
|
1739
|
+
* `ambiguous` carries the count. This is `pickBestWikilinkTarget`
|
|
1740
|
+
* (`src/wikilinkResolve.ts`) reimplemented over page ids: core cannot
|
|
1741
|
+
* import from `src/` and the original uses Node's `path`. Matching other
|
|
1742
|
+
* active bases is the loop's job; `candidates` says how many matched here.
|
|
1743
|
+
*/
|
|
1744
|
+
|
|
1745
|
+
type AiDocsLinkStatus = 'ok' | 'broken' | 'external';
|
|
1746
|
+
type AiDocsBrokenReason = 'outside-root' | 'not-found';
|
|
1747
|
+
interface AiDocsResolvedLink {
|
|
1748
|
+
text: string;
|
|
1749
|
+
target: string;
|
|
1750
|
+
kind: AiDocsLink['kind'];
|
|
1751
|
+
status: AiDocsLinkStatus;
|
|
1752
|
+
reason?: AiDocsBrokenReason;
|
|
1753
|
+
/** The page the link lands on (status `ok`). */
|
|
1754
|
+
pageId?: string;
|
|
1755
|
+
/** The section it lands on, when the fragment matched a heading. */
|
|
1756
|
+
sectionId?: string;
|
|
1757
|
+
/** The page is real but the `#fragment` matched no heading. */
|
|
1758
|
+
fragmentMissing?: boolean;
|
|
1759
|
+
/** Wikilink only: how many pages matched the name in this base. */
|
|
1760
|
+
candidates?: number;
|
|
1761
|
+
}
|
|
1762
|
+
interface DocsLinkResolver {
|
|
1763
|
+
resolve(fromPageId: string, link: AiDocsLink): AiDocsResolvedLink;
|
|
1764
|
+
/**
|
|
1765
|
+
* Links that are `broken`, over every page of the index. The work on
|
|
1766
|
+
* wikilinks is bounded (`DOCS_MAX_LINK_WORK` candidate pages); past it the
|
|
1767
|
+
* count stops, so it is a lower bound for a hostile base.
|
|
1768
|
+
*/
|
|
1769
|
+
countBroken(): number;
|
|
1770
|
+
/** Broken links on one page, for a host that counts a page at a time and yields between pages. Shares the work bound. */
|
|
1771
|
+
countBrokenOn(pageId: string): number;
|
|
1772
|
+
/** The wikilink work bound was hit, so `countBroken` is a lower bound. */
|
|
1773
|
+
workExceeded(): boolean;
|
|
1774
|
+
}
|
|
1775
|
+
/** Order wikilink candidates nearest first (see the header). Pure and total. */
|
|
1776
|
+
declare function rankDocsWikilinkCandidates(fromPageId: string, candidateIds: readonly string[]): string[];
|
|
1777
|
+
declare function createDocsLinkResolver(index: Pick<AiDocsIndex, 'pages'>): DocsLinkResolver;
|
|
1778
|
+
interface DocsLinkList {
|
|
1779
|
+
/** Followable links, in page order, one per landing (page or section). */
|
|
1780
|
+
links: AiDocsResolvedLink[];
|
|
1781
|
+
/** Followable links beyond the cap. */
|
|
1782
|
+
omitted: number;
|
|
1783
|
+
/** Links on the page that are broken (not listed). */
|
|
1784
|
+
broken: number;
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* The hop-1 link list of a page (usually a home): its followable links,
|
|
1788
|
+
* deduplicated by landing, at most `cap` (200). Broken and external links are
|
|
1789
|
+
* left out; broken ones are counted.
|
|
1790
|
+
*/
|
|
1791
|
+
declare function docsLinkList(index: Pick<AiDocsIndex, 'pages'>, pageId: string, cap?: number, resolver?: DocsLinkResolver): DocsLinkList;
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Keyword fallback ranking (decision 11): when a root's home is the map, or
|
|
1795
|
+
* navigation read nothing, hosts scan their files and rank sections with this,
|
|
1796
|
+
* so both hosts return the same order for the same input. Pure.
|
|
1797
|
+
*
|
|
1798
|
+
* THE RANKING CONTRACT (A10; `test/ai-fixtures/docs/keyword.json`). All
|
|
1799
|
+
* arithmetic is integer, all comparison is by code point; nothing depends on
|
|
1800
|
+
* locale.
|
|
1801
|
+
*
|
|
1802
|
+
* Case folding. ASCII `A-Z` become `a-z`; every other code point is left
|
|
1803
|
+
* unchanged (`foldAscii`). `É` does not match `é`.
|
|
1804
|
+
*
|
|
1805
|
+
* Terms. Fold the query, then split it into tokens: a token is a maximal run
|
|
1806
|
+
* of ASCII letters and digits, `_`, and any non-ASCII code point that is not
|
|
1807
|
+
* in the fixed whitespace list (`isDocsSpace`). Everything else separates
|
|
1808
|
+
* tokens (so `key-rotation` is `key`, `rotation`). Drop tokens in `STOP_WORDS`
|
|
1809
|
+
* unless that leaves none (then keep every token). Remove repeats (first
|
|
1810
|
+
* occurrence stays), keep the first `DOCS_KEYWORD_MAX_TERMS` (12). No terms,
|
|
1811
|
+
* no results.
|
|
1812
|
+
*
|
|
1813
|
+
* Fields of a section: its heading text (none for a preamble), its page title,
|
|
1814
|
+
* its page rel path, and its text. A term "occurs" in a field when the folded
|
|
1815
|
+
* field contains it as a substring (so `rotat` finds `rotation`); occurrences
|
|
1816
|
+
* are counted left to right without overlap.
|
|
1817
|
+
*
|
|
1818
|
+
* Score, per section, over all terms:
|
|
1819
|
+
* 10 x (terms occurring in the heading)
|
|
1820
|
+
* + 3 x (terms occurring in the page title)
|
|
1821
|
+
* + 2 x (terms occurring in the rel path)
|
|
1822
|
+
* + sum over terms of min(occurrences in the text, 5)
|
|
1823
|
+
* + 6 x (terms occurring in any of the four fields)
|
|
1824
|
+
* A score of 0 means no match and the section is left out.
|
|
1825
|
+
*
|
|
1826
|
+
* Order. Score descending; ties broken by section id ascending by code point
|
|
1827
|
+
* (ids are unique). The first `DOCS_KEYWORD_TOP` (20) are returned.
|
|
1828
|
+
*/
|
|
1829
|
+
|
|
1830
|
+
/** Words that carry no meaning in a question. Fixed list; the same on every host. */
|
|
1831
|
+
declare const DOCS_STOP_WORDS: readonly string[];
|
|
1832
|
+
/** Fold and split a query into ranking terms (see the header). */
|
|
1833
|
+
declare function keywordTerms(query: string): string[];
|
|
1834
|
+
/** Non-overlapping occurrences of `term` in `text`, left to right. */
|
|
1835
|
+
declare function countOccurrences(text: string, term: string): number;
|
|
1836
|
+
/** A section as the host scans it. */
|
|
1837
|
+
interface DocsKeywordSection {
|
|
1838
|
+
/** `<page id>#<slug>`, or the page id for a preamble. */
|
|
1839
|
+
id: string;
|
|
1840
|
+
pageId: string;
|
|
1841
|
+
/** The page's title. */
|
|
1842
|
+
title: string;
|
|
1843
|
+
relPath: string;
|
|
1844
|
+
/** The heading text; absent for a preamble. */
|
|
1845
|
+
heading?: string;
|
|
1846
|
+
text: string;
|
|
1847
|
+
}
|
|
1848
|
+
/** Score one section against folded terms (0 is no match). */
|
|
1849
|
+
declare function scoreKeywordSection(terms: readonly string[], section: DocsKeywordSection): number;
|
|
1850
|
+
/** Rank sections for a query: scored, ordered, top `limit` (20). */
|
|
1851
|
+
declare function rankKeywordSections(query: string, sections: readonly DocsKeywordSection[], limit?: number): AiDocsSectionRef[];
|
|
1852
|
+
|
|
1853
|
+
/**
|
|
1854
|
+
* Secrets scan (S7, A6): strict patterns, run on every string that leaves the
|
|
1855
|
+
* machine for a model, on every engine. A matching section or entry is left
|
|
1856
|
+
* out and counted in the coverage note. No lookbehind (older WebKit rejects
|
|
1857
|
+
* the whole module). Pure, linear-time (each pattern is
|
|
1858
|
+
* anchored on a literal prefix and uses bounded or possessive-by-construction
|
|
1859
|
+
* runs; `test/…` runs 1 MiB adversarial inputs).
|
|
1860
|
+
*
|
|
1861
|
+
* Patterns (a match anywhere in the text):
|
|
1862
|
+
* - aws-access-key: `AKIA` or `ASIA` + 16 of `[0-9A-Z]`, not inside a longer
|
|
1863
|
+
* alphanumeric run.
|
|
1864
|
+
* - api-token: `sk-` (optionally `sk-ant-`, `sk-proj-` ...) followed by a run
|
|
1865
|
+
* of at least 20 of `[A-Za-z0-9_-]` that contains a digit, and only where
|
|
1866
|
+
* `sk-` does not continue a word, so `task-based`, `risk-free` and
|
|
1867
|
+
* `desk-mounted` never match; or Stripe style `sk_live_` / `sk_test_` /
|
|
1868
|
+
* `rk_live_` / `rk_test_` + 16 or more alphanumerics.
|
|
1869
|
+
* - private-key: any `-----BEGIN ... PRIVATE KEY-----` header (RSA, EC,
|
|
1870
|
+
* DSA, OPENSSH, ENCRYPTED, bare, `PGP PRIVATE KEY BLOCK`). The header is
|
|
1871
|
+
* enough; the body need not be present. Whitespace between the words of
|
|
1872
|
+
* the header may include a line break (a header re-wrapped by a tool).
|
|
1873
|
+
* - jwt: `eyJ...` `.` `eyJ...` `.` signature (each part base64url; header
|
|
1874
|
+
* and payload at least 8 characters after `eyJ`; the signature may be
|
|
1875
|
+
* empty, for unsecured tokens).
|
|
1876
|
+
*
|
|
1877
|
+
* A section the host cut at the byte cap (`truncated`) may have lost the end
|
|
1878
|
+
* of a key. Hosts scan the uncapped text when they can (`capSectionText`
|
|
1879
|
+
* says so); for what arrives already cut, `scanDocsSections` also applies the
|
|
1880
|
+
* tail rule (`findSecretAtCutoff`): a key that starts in the last stretch of
|
|
1881
|
+
* the text and runs to its end counts.
|
|
1882
|
+
*/
|
|
1883
|
+
|
|
1884
|
+
type DocsSecretKind = 'aws-access-key' | 'api-token' | 'private-key' | 'jwt';
|
|
1885
|
+
/** What kind of secret `text` contains, or null. The string-level check (A6). */
|
|
1886
|
+
declare function findSecret(text: string): DocsSecretKind | null;
|
|
1887
|
+
declare function containsSecret(text: string): boolean;
|
|
1888
|
+
/**
|
|
1889
|
+
* For text that was cut at the cap: a secret that begins near the end and runs
|
|
1890
|
+
* off it (half a key is still a key). Looks only at the last 256 characters.
|
|
1891
|
+
*/
|
|
1892
|
+
declare function findSecretAtCutoff(text: string): DocsSecretKind | null;
|
|
1893
|
+
interface SecretScan<T> {
|
|
1894
|
+
/** Items with no secret, in the order given. */
|
|
1895
|
+
kept: T[];
|
|
1896
|
+
/** Ids of the items left out. Ids only: never the text (S13). */
|
|
1897
|
+
leftOut: string[];
|
|
1898
|
+
/** Why each was left out, by id. */
|
|
1899
|
+
kinds: Record<string, DocsSecretKind>;
|
|
1900
|
+
}
|
|
1901
|
+
/** Scan any list of items. `textOf` returns every string of the item that would be sent. */
|
|
1902
|
+
declare function scanForSecrets<T extends {
|
|
1903
|
+
id: string;
|
|
1904
|
+
}>(items: readonly T[], textOf: (item: T) => string | readonly string[], truncatedOf?: (item: T) => boolean): SecretScan<T>;
|
|
1905
|
+
/** Scan the sections `readSections` returned. */
|
|
1906
|
+
declare function scanDocsSections(sections: readonly AiDocsSection[]): SecretScan<AiDocsSection>;
|
|
1907
|
+
/**
|
|
1908
|
+
* This-document scope: scan the live text per section, drop the sections that
|
|
1909
|
+
* hold a key and keep the rest, so one key does not drop the whole document.
|
|
1910
|
+
* `text` is the kept sections joined as they were (in file order).
|
|
1911
|
+
*/
|
|
1912
|
+
declare function dropSecretSections(markdown: string): {
|
|
1913
|
+
text: string;
|
|
1914
|
+
total: number;
|
|
1915
|
+
leftOut: number;
|
|
1916
|
+
};
|
|
1917
|
+
/**
|
|
1918
|
+
* The coverage phrase: "1 section left out: looks like it contains a key".
|
|
1919
|
+
* `noun` is what was left out (`section`, or `entry` for navigation lists).
|
|
1920
|
+
*/
|
|
1921
|
+
declare function secretsLeftOutPhrase(count: number, noun?: 'section' | 'entry'): string;
|
|
1922
|
+
|
|
1923
|
+
/**
|
|
1924
|
+
* The answer call's source budget: keep whole sections in the order asked
|
|
1925
|
+
* until the share of the route's context is used, then number what was kept
|
|
1926
|
+
* and write the coverage note. Pure. Built on `documentBudget.ts`
|
|
1927
|
+
* (`CHARS_PER_TOKEN`).
|
|
1928
|
+
*
|
|
1929
|
+
* Rules.
|
|
1930
|
+
* - Sections are deduplicated by id (first stays) and taken in the order
|
|
1931
|
+
* given: the loop puts the most relevant first.
|
|
1932
|
+
* - The budget is `floor(contextTokens x sourceShare / 100 x
|
|
1933
|
+
* CHARS_PER_TOKEN)` characters. A section that fits the room left is kept;
|
|
1934
|
+
* one that does not is left out and the next is tried, so a huge section
|
|
1935
|
+
* cannot starve smaller ones.
|
|
1936
|
+
* - If nothing fits whole, the first section is kept cut to the budget (on a
|
|
1937
|
+
* code point boundary, `truncated` set), so an answer always has something.
|
|
1938
|
+
* - NUMBERING HAPPENS AFTER THE CUT: `n` runs 1..k over the sections that
|
|
1939
|
+
* are actually sent, in the order kept. A model never sees a gap.
|
|
1940
|
+
* - Callers scan for secrets first (`scanDocsSections`) and pass how many
|
|
1941
|
+
* sections that left out, so the note can say so.
|
|
1942
|
+
*/
|
|
1943
|
+
|
|
1944
|
+
interface DocsBudgetOptions {
|
|
1945
|
+
/** The route's context window, in tokens. */
|
|
1946
|
+
contextTokens: number;
|
|
1947
|
+
/** Percent of it the sections may take. Clamped 30 to 80 (default 60). */
|
|
1948
|
+
sourceShare?: number;
|
|
1949
|
+
/** Sections the secrets scan already left out. */
|
|
1950
|
+
secretsLeftOut?: number;
|
|
1951
|
+
}
|
|
1952
|
+
interface AiDocsNumberedSection {
|
|
1953
|
+
/** 1-based, contiguous over what is sent. */
|
|
1954
|
+
n: number;
|
|
1955
|
+
section: AiDocsSection;
|
|
1956
|
+
}
|
|
1957
|
+
interface DocsBudgetResult {
|
|
1958
|
+
/** What goes to the model, numbered. */
|
|
1959
|
+
sent: AiDocsNumberedSection[];
|
|
1960
|
+
/** Ids left out because they did not fit. */
|
|
1961
|
+
cutIds: string[];
|
|
1962
|
+
/** Sections asked for, after dedupe and without the secret ones. */
|
|
1963
|
+
requested: number;
|
|
1964
|
+
/** Sections sent that the host or the budget cut short. */
|
|
1965
|
+
cutShort: number;
|
|
1966
|
+
/** Characters the budget allowed. */
|
|
1967
|
+
budgetChars: number;
|
|
1968
|
+
/** "answered from 4 of 7 sections, 3 left out", or null when no section was sent or asked for. */
|
|
1969
|
+
note: string | null;
|
|
1970
|
+
}
|
|
1971
|
+
/** Characters of source text a route allows. */
|
|
1972
|
+
declare function docsBudgetChars(contextTokens: number, sourceShare?: number): number;
|
|
1973
|
+
/** The coverage note (without the secrets part when `secretsLeftOut` is 0). */
|
|
1974
|
+
declare function docsCoverageNote(total: number, used: number, cutShort: number, secretsLeftOut: number): string | null;
|
|
1975
|
+
declare function budgetDocsSections(sections: readonly AiDocsSection[], opts: DocsBudgetOptions): DocsBudgetResult;
|
|
1976
|
+
|
|
1977
|
+
/**
|
|
1978
|
+
* Citations in an answer, and what Insert at cursor does with it. Pure.
|
|
1979
|
+
*
|
|
1980
|
+
* Parsing (`parseCitations`). The model cites sources as `[n]` or `[n, m]`
|
|
1981
|
+
* (the numbers the final call gave its sections). A marker is NOT a citation
|
|
1982
|
+
* when it sits in a fenced code block or code span, is followed by `(` or `:`
|
|
1983
|
+
* (a link or a definition), follows `!` or a non-citation `]` (an image or
|
|
1984
|
+
* `[text][1]` reference). Unknown numbers are dropped from the text (and from
|
|
1985
|
+
* a list); none valid means `NO_SOURCES_CITED`.
|
|
1986
|
+
*
|
|
1987
|
+
* Insert (`prepareInsertMarkdown`, A8/S8). THE SECURITY BOUNDARY IS NOT HERE. It
|
|
1988
|
+
* is the parsed walk in `ai-insert-guard.ts`, which runs on the editor's own
|
|
1989
|
+
* parser output and enforces `InsertUrlPolicy`. A string-level pass approximates
|
|
1990
|
+
* CommonMark, and wherever it disagrees with markdown-it, markup gets through.
|
|
1991
|
+
* Treat what follows as a first pass that keeps the common cases readable (in
|
|
1992
|
+
* source mode above all), and never rely on it to stop anything.
|
|
1993
|
+
* Two steps, in this order:
|
|
1994
|
+
* 1. `sanitizeInsertMarkdown`: every link, image, reference-style link and
|
|
1995
|
+
* bare or `<...>` autolink the MODEL wrote becomes plain text unless its
|
|
1996
|
+
* URL appears verbatim in a cited section's text (and is `https:`,
|
|
1997
|
+
* `http:` or, for links, `mailto:`). A link around an image is handled
|
|
1998
|
+
* innermost first, so nesting cannot carry an image through. Raw HTML
|
|
1999
|
+
* tags are escaped. Code spans and fenced blocks are left alone.
|
|
2000
|
+
* 2. `rewriteCitationsForInsert`: each citation becomes a CommonMark
|
|
2001
|
+
* relative link to the cited file, such as
|
|
2002
|
+
* `[Key rotation](security.md#key-rotation)` (spaces and brackets in
|
|
2003
|
+
* the path percent-encoded), when the current document sits under the
|
|
2004
|
+
* same root as the source; otherwise plain `file › heading` text. The
|
|
2005
|
+
* fragment is the heading slug (`docsSlug`), which is GitHub's anchor
|
|
2006
|
+
* for ASCII headings.
|
|
2007
|
+
* Those relative links are the only links Insert adds.
|
|
2008
|
+
*/
|
|
2009
|
+
|
|
2010
|
+
declare const NO_SOURCES_CITED = "No sources cited";
|
|
2011
|
+
declare const ANSWERED_WITHOUT_SOURCES = "answered without sources";
|
|
2012
|
+
type DocsCitationPart = {
|
|
2013
|
+
type: 'text';
|
|
2014
|
+
text: string;
|
|
2015
|
+
} | {
|
|
2016
|
+
type: 'cite';
|
|
2017
|
+
ns: number[];
|
|
2018
|
+
};
|
|
2019
|
+
interface ParsedCitations {
|
|
2020
|
+
/** Valid numbers in order of first appearance. */
|
|
2021
|
+
cited: number[];
|
|
2022
|
+
/** Numbers the model wrote that match no source, in order, once each. */
|
|
2023
|
+
dropped: number[];
|
|
2024
|
+
/** The answer with unknown numbers removed. */
|
|
2025
|
+
text: string;
|
|
2026
|
+
/** The same answer cut into plain text and citation parts, for rendering `[1]` chips. */
|
|
2027
|
+
parts: DocsCitationPart[];
|
|
2028
|
+
/** True when no valid citation is left. Show `NO_SOURCES_CITED`. */
|
|
2029
|
+
noSources: boolean;
|
|
2030
|
+
}
|
|
2031
|
+
/** Find `[n]` markers and keep the valid ones. `validNs` are the numbers of the sections sent. */
|
|
2032
|
+
declare function parseCitations(answer: string, validNs: Iterable<number>): ParsedCitations;
|
|
2033
|
+
/** The slug part of a section id (`undefined` for a whole page). */
|
|
2034
|
+
declare function docsSectionSlug(section: Pick<AiDocsSection, 'id' | 'pageId'>): string | undefined;
|
|
2035
|
+
/** The chip text: `file › heading`, or the file alone for a whole page. */
|
|
2036
|
+
declare function docsCitationLabel(section: Pick<AiDocsSection, 'relPath' | 'heading'>): string;
|
|
2037
|
+
/**
|
|
2038
|
+
* Percent-encode what would end a CommonMark link destination or change what
|
|
2039
|
+
* it points at: space, `%`, `#` (a file named `a#b.md` is not `a` plus a
|
|
2040
|
+
* fragment), `?`, brackets, parentheses, angle brackets, backslash, control
|
|
2041
|
+
* characters.
|
|
2042
|
+
*/
|
|
2043
|
+
declare function encodeLinkPath(path: string): string;
|
|
2044
|
+
/** Where the document being edited sits: the label of the root that contains it and its folder relative to that root (`''` for the root itself). */
|
|
2045
|
+
interface DocsDocumentLocation {
|
|
2046
|
+
root: string;
|
|
2047
|
+
folder: string;
|
|
2048
|
+
/** The base of that root. Labels are unique per base only, so a source from another base is not linked. */
|
|
2049
|
+
baseId?: string;
|
|
2050
|
+
}
|
|
2051
|
+
/** Relative path from a folder (relative to a root) to a file (relative to the same root). */
|
|
2052
|
+
declare function relativeDocsPath(fromFolder: string, toRelPath: string): string;
|
|
2053
|
+
/**
|
|
2054
|
+
* The CommonMark relative link for one cited section, or null when the
|
|
2055
|
+
* document is not under the section's root (then the caller writes plain text).
|
|
2056
|
+
*/
|
|
2057
|
+
declare function citationLink(section: AiDocsSection, location: DocsDocumentLocation | null | undefined): string | null;
|
|
2058
|
+
/**
|
|
2059
|
+
* Replace each valid `[n]` with its link (or `file › heading` text). Unknown
|
|
2060
|
+
* numbers are removed. Run `sanitizeInsertMarkdown` first.
|
|
2061
|
+
*/
|
|
2062
|
+
declare function rewriteCitationsForInsert(answer: string, sources: ReadonlyArray<AiDocsNumberedSection & {
|
|
2063
|
+
baseId?: string;
|
|
2064
|
+
}>, location?: DocsDocumentLocation | null): string;
|
|
2065
|
+
/**
|
|
2066
|
+
* What may stay live in inserted content (S8), enforced on the parsed result by
|
|
2067
|
+
* `ai-insert-guard.ts`: citation links built from the sections sent, and URLs
|
|
2068
|
+
* that appear verbatim in a cited section.
|
|
2069
|
+
*/
|
|
2070
|
+
interface InsertUrlPolicy {
|
|
2071
|
+
/** Destinations of the citation links, as written in the markdown (before the parser normalizes them). */
|
|
2072
|
+
citationHrefs: readonly string[];
|
|
2073
|
+
citedTexts: readonly string[];
|
|
2074
|
+
}
|
|
2075
|
+
/** The verbatim half of `InsertUrlPolicy`: a link (`https:`, `http:`, `mailto:`) or image (`https:`) URL found in a cited section. */
|
|
2076
|
+
declare function insertUrlIsVerbatim(policy: InsertUrlPolicy, url: string, kind: 'link' | 'image'): boolean;
|
|
2077
|
+
interface SanitizedMarkdown {
|
|
2078
|
+
markdown: string;
|
|
2079
|
+
/** Links, autolinks and definitions turned into text. */
|
|
2080
|
+
linksRemoved: number;
|
|
2081
|
+
imagesRemoved: number;
|
|
2082
|
+
}
|
|
2083
|
+
/** See the header. `citedTexts` are the texts of the sections the answer cited. */
|
|
2084
|
+
declare function sanitizeInsertMarkdown(markdown: string, citedTexts: readonly string[]): SanitizedMarkdown;
|
|
2085
|
+
/**
|
|
2086
|
+
* The text Insert at cursor puts in the document: sanitize what the model
|
|
2087
|
+
* wrote, then turn citations into relative links (or `file › heading`). Also
|
|
2088
|
+
* returns the `policy` the caller must hand to the insert, whose parsed walk is
|
|
2089
|
+
* what actually keeps anything else out.
|
|
2090
|
+
*/
|
|
2091
|
+
declare function prepareInsertMarkdown(answer: string, sources: ReadonlyArray<AiDocsNumberedSection & {
|
|
2092
|
+
baseId?: string;
|
|
2093
|
+
}>, location?: DocsDocumentLocation | null): SanitizedMarkdown & {
|
|
2094
|
+
policy: InsertUrlPolicy;
|
|
2095
|
+
};
|
|
2096
|
+
|
|
2097
|
+
/**
|
|
2098
|
+
* Getting from a cited section back to its file, and from the document being
|
|
2099
|
+
* edited to a base. Pure, for the chat panel and the editor's reveal.
|
|
2100
|
+
*
|
|
2101
|
+
* A chip's target comes from `host.docs.listBases()` (the root) and the section
|
|
2102
|
+
* the host read (`relPath`), never from model text. The href is a `file:` URL
|
|
2103
|
+
* because both hosts need an absolute path outside the current folder, and a
|
|
2104
|
+
* Windows path would read as a URL scheme. Hosts open it after their own checks.
|
|
2105
|
+
*
|
|
2106
|
+
* A heading is found again by M2's slug rule (`docs/index.ts`), not GitHub's:
|
|
2107
|
+
* the two differ on non-ASCII text and on some duplicates.
|
|
2108
|
+
*/
|
|
2109
|
+
|
|
2110
|
+
/** `file:` URL for `relPath` under an absolute root: POSIX, drive letter or UNC. */
|
|
2111
|
+
declare function docsFileHref(root: string, relPath: string): string;
|
|
2112
|
+
/** The absolute path a section's file has, for display. Null when its base or root is no longer configured. */
|
|
2113
|
+
declare function docsSectionFile(section: Pick<AiDocsSection, 'pageId' | 'relPath'>, baseId: string, statuses: readonly AiDocBaseStatus[]): {
|
|
2114
|
+
root: string;
|
|
2115
|
+
relPath: string;
|
|
2116
|
+
baseName: string;
|
|
2117
|
+
} | null;
|
|
2118
|
+
/**
|
|
2119
|
+
* Where a folder sits among the bases, for Insert's relative links. Null for a
|
|
2120
|
+
* folder that is not absolute (an untitled document) or under no root. A base
|
|
2121
|
+
* in `prefer` wins, so a folder under two bases links to the one the answer used.
|
|
2122
|
+
*/
|
|
2123
|
+
declare function docsDocumentLocation(folder: string | null | undefined, statuses: readonly AiDocBaseStatus[], prefer?: readonly string[]): DocsDocumentLocation | null;
|
|
2124
|
+
interface DocsHeadingHit {
|
|
2125
|
+
/** Which heading, 0-based, counting the headings `headingsOutline` reports. */
|
|
2126
|
+
index: number;
|
|
2127
|
+
/** Where its line starts in `markdown`. */
|
|
2128
|
+
offset: number;
|
|
2129
|
+
}
|
|
2130
|
+
/** The heading whose M2 slug is `slug`, or null. */
|
|
2131
|
+
declare function docsHeadingAt(markdown: string, slug: string): DocsHeadingHit | null;
|
|
2132
|
+
|
|
2133
|
+
/**
|
|
2134
|
+
* The documentation chat's turn: choose one route, navigate by outlines, read
|
|
2135
|
+
* sections, answer with citations. The security model rests here, so the rules
|
|
2136
|
+
* are stated once and enforced in code, not in a prompt.
|
|
2137
|
+
*
|
|
2138
|
+
* - One route per turn (decision 7, A4). The route is resolved once, the
|
|
2139
|
+
* per-base cloud switch is checked on it (one reached `viaFallback` too),
|
|
2140
|
+
* and only then may a consent dialog or a request happen. Every hop and the
|
|
2141
|
+
* answer carry the pin, so an engine going down mid-turn fails the turn; it
|
|
2142
|
+
* never moves pages to another engine. The next turn resolves afresh.
|
|
2143
|
+
* - Fail closed (S6). No route (a tier-1 host routes by itself) counts as not
|
|
2144
|
+
* local. `allowCloud` comes from `listBases()` for this turn, never a copy.
|
|
2145
|
+
* - The model names ids and nothing else (S1). Its reply is hostile: `read`
|
|
2146
|
+
* may be missing, not a list, full of non-strings or thousands long. Only
|
|
2147
|
+
* ids found in an active base's index reach `readSections`, and what is
|
|
2148
|
+
* passed on is the index's own string, not the model's.
|
|
2149
|
+
* - Ids are unique per base only, so a read is always `(baseId, id)`. With
|
|
2150
|
+
* more than one base the ids offered carry a `b1:` token.
|
|
2151
|
+
* - Secrets (S7, A6, A7). Every string is scanned before any message is
|
|
2152
|
+
* built: section text and labels, titles, headings, link text, keyword
|
|
2153
|
+
* labels, the question. The Activity log records messages verbatim, so what
|
|
2154
|
+
* the scan leaves out can reach neither a request nor an entry.
|
|
2155
|
+
* - Nothing from a page changes the control flow (S10). Only the model's JSON,
|
|
2156
|
+
* filtered through the index, picks what is read. The answer call's output
|
|
2157
|
+
* is never parsed for further reads, and the hop cap holds regardless.
|
|
2158
|
+
* - Read-only (S12). No writes, no shell, no URLs: the loop's only effects are
|
|
2159
|
+
* `deps` and the read calls of `host.docs`.
|
|
2160
|
+
*
|
|
2161
|
+
* Pure: `ai/` has no DOM and no network, and `ai-request.ts` sits outside it,
|
|
2162
|
+
* so the completion runner and route resolution are parameters
|
|
2163
|
+
* (`ai-docs-run.ts` is the wiring).
|
|
2164
|
+
*/
|
|
2165
|
+
|
|
2166
|
+
declare const DOCS_CHAT_FEATURE = "chat";
|
|
2167
|
+
/** The This document scope's synthetic page. */
|
|
2168
|
+
declare const DOCS_DOCUMENT_BASE_ID = "document";
|
|
2169
|
+
/**
|
|
2170
|
+
* A route held for a whole turn (A4): the pair `runAiCompletion` must use, with
|
|
2171
|
+
* no re-routing and no retry elsewhere.
|
|
2172
|
+
*/
|
|
2173
|
+
interface AiRoutePin {
|
|
2174
|
+
engineId: string;
|
|
2175
|
+
modelId: string;
|
|
2176
|
+
/** How the turn's route was reached, for the badge and the Activity entry. */
|
|
2177
|
+
viaFallback?: boolean;
|
|
2178
|
+
reason?: string;
|
|
2179
|
+
/** "Allow once" answered for this turn: it covers every call of this turn, and nothing after it. */
|
|
2180
|
+
consented?: boolean;
|
|
2181
|
+
/** The engine's locality when the turn was routed. A pinned call fails if it has changed. */
|
|
2182
|
+
isLocal?: boolean;
|
|
2183
|
+
/** The turn, on the wire with every request (`AiCompletionTurn`), so a host can hold "Allow once" for exactly this turn. */
|
|
2184
|
+
turnId?: string;
|
|
2185
|
+
}
|
|
2186
|
+
/** What a chat request's Activity entry adds to the usual fields (A7). Ids only, never text. */
|
|
2187
|
+
interface AiDocsActivityDetail {
|
|
2188
|
+
/** Which navigation step, 1-based. Absent on the answer. */
|
|
2189
|
+
hop?: number;
|
|
2190
|
+
/** Navigate: the sections this hop's reply read. Answer: the sections sent. */
|
|
2191
|
+
pages: string[];
|
|
2192
|
+
/** Ids the model named that were not used, clipped and at most `DOCS_MAX_ACTIVITY_IDS`. */
|
|
2193
|
+
droppedIds: string[];
|
|
2194
|
+
/** Everything dropped, non-strings and overflow included. */
|
|
2195
|
+
droppedCount: number;
|
|
2196
|
+
/** Sections the secrets scan left out. */
|
|
2197
|
+
leftOutIds?: string[];
|
|
2198
|
+
/** Sections that did not fit the budget. */
|
|
2199
|
+
cutIds?: string[];
|
|
2200
|
+
}
|
|
2201
|
+
interface DocsRunRequest {
|
|
2202
|
+
feature: string;
|
|
2203
|
+
task: string;
|
|
2204
|
+
sends: AiSendsScope;
|
|
2205
|
+
baseIds?: string[];
|
|
2206
|
+
messages: AiMessage[];
|
|
2207
|
+
maxTokens: number;
|
|
2208
|
+
temperature: number;
|
|
2209
|
+
format?: 'json';
|
|
2210
|
+
job: 'navigate' | 'answer';
|
|
2211
|
+
thinking?: AiThinkingLevel | null | ((kind: string) => AiThinkingLevel | null);
|
|
2212
|
+
pin?: AiRoutePin;
|
|
2213
|
+
docs: AiDocsActivityDetail;
|
|
2214
|
+
}
|
|
2215
|
+
/** The thinking state of the request, as the runner reports it. */
|
|
2216
|
+
interface DocsRunThinking {
|
|
2217
|
+
kind: string;
|
|
2218
|
+
configured: AiThinkingLevel | null;
|
|
2219
|
+
sent: AiThinkingLevel | null;
|
|
2220
|
+
auto?: boolean;
|
|
2221
|
+
}
|
|
2222
|
+
interface DocsRunOutcome {
|
|
2223
|
+
result: AiCompletionResult;
|
|
2224
|
+
route: AiRouteOk | null;
|
|
2225
|
+
viaFallback: boolean;
|
|
2226
|
+
thinking: DocsRunThinking | null;
|
|
2227
|
+
/** The Activity entry's id, for `annotate`. */
|
|
2228
|
+
requestId?: string;
|
|
2229
|
+
}
|
|
2230
|
+
interface DocsRunEvents {
|
|
2231
|
+
onRoute?(route: AiRouteOk | null, thinking: DocsRunThinking | null): void;
|
|
2232
|
+
onToken?(delta: string, text: string): void;
|
|
2233
|
+
}
|
|
2234
|
+
interface DocsRouteRequest {
|
|
2235
|
+
feature: string;
|
|
2236
|
+
task: string;
|
|
2237
|
+
sends: AiSendsScope;
|
|
2238
|
+
baseIds?: string[];
|
|
2239
|
+
/** Asked about every route considered, before any consent dialog. False stops the turn with `blocked`. */
|
|
2240
|
+
allowed(route: AiRouteOk | null): boolean;
|
|
2241
|
+
}
|
|
2242
|
+
interface DocsTurnRoute {
|
|
2243
|
+
/** `allowed` said no; nothing was asked and nothing was sent. */
|
|
2244
|
+
blocked: boolean;
|
|
2245
|
+
/** Null for a tier-1 host. */
|
|
2246
|
+
route: AiRouteOk | null;
|
|
2247
|
+
/** Null when there is no route to hold. */
|
|
2248
|
+
pin: AiRoutePin | null;
|
|
2249
|
+
}
|
|
2250
|
+
/** What the loop needs from the editor's request layer (`aiDocsLoopDeps`). */
|
|
2251
|
+
interface DocsLoopDeps {
|
|
2252
|
+
resolveRoute(req: DocsRouteRequest, signal: AbortSignal): Promise<DocsTurnRoute>;
|
|
2253
|
+
run(req: DocsRunRequest, events: DocsRunEvents, signal: AbortSignal): Promise<DocsRunOutcome>;
|
|
2254
|
+
/** The route's context window, null when unknown. */
|
|
2255
|
+
contextTokens(route: AiRouteOk | null): number | null;
|
|
2256
|
+
/** Update a request's Activity entry once the hop's reply was acted on. */
|
|
2257
|
+
annotate?(requestId: string, detail: AiDocsActivityDetail): void;
|
|
2258
|
+
/** The turn holding `pin` is over, however it ended. */
|
|
2259
|
+
endTurn?(pin: AiRoutePin): void;
|
|
2260
|
+
}
|
|
2261
|
+
/** The read side of `host.docs`; the loop never touches `saveBases` or a folder picker. */
|
|
2262
|
+
type DocsReadHost = Pick<MikeDownDocsHost, 'listBases' | 'index' | 'readSections' | 'keywordSearch'>;
|
|
2263
|
+
type DocsTurnScope = {
|
|
2264
|
+
kind: 'bases';
|
|
2265
|
+
docs: DocsReadHost;
|
|
2266
|
+
options?: Partial<AiDocsOptions>;
|
|
2267
|
+
}
|
|
2268
|
+
/** Live editor text: a single synthetic page, no host calls, task `chat`. */
|
|
2269
|
+
| {
|
|
2270
|
+
kind: 'document';
|
|
2271
|
+
text: string;
|
|
2272
|
+
title?: string;
|
|
2273
|
+
options?: Partial<AiDocsOptions>;
|
|
2274
|
+
};
|
|
2275
|
+
interface DocsTurnInput {
|
|
2276
|
+
question: string;
|
|
2277
|
+
scope: DocsTurnScope;
|
|
2278
|
+
/** The user's dial, for the answer only: navigation stays on its job default (Off). */
|
|
2279
|
+
thinking?: DocsRunRequest['thinking'];
|
|
2280
|
+
}
|
|
2281
|
+
interface DocsTurnProgress {
|
|
2282
|
+
phase: 'navigate' | 'keyword' | 'answer';
|
|
2283
|
+
/** Navigation step, 1-based. */
|
|
2284
|
+
hop?: number;
|
|
2285
|
+
maxHops: number;
|
|
2286
|
+
/** Sections read so far. */
|
|
2287
|
+
pages: number;
|
|
2288
|
+
}
|
|
2289
|
+
interface DocsTurnEvents extends DocsRunEvents {
|
|
2290
|
+
onProgress?(progress: DocsTurnProgress): void;
|
|
2291
|
+
}
|
|
2292
|
+
/** A numbered section as sent, with the base it came from: ids are unique per base only. */
|
|
2293
|
+
interface DocsSentSection extends AiDocsNumberedSection {
|
|
2294
|
+
baseId: string;
|
|
2295
|
+
}
|
|
2296
|
+
interface DocsTurnAnswer {
|
|
2297
|
+
kind: 'answered';
|
|
2298
|
+
/** The answer with unknown citation numbers removed and reasoning stripped. */
|
|
2299
|
+
answer: string;
|
|
2300
|
+
citations: ParsedCitations;
|
|
2301
|
+
sent: DocsSentSection[];
|
|
2302
|
+
/** The note under the answer: coverage, secrets left out, or "answered without sources". */
|
|
2303
|
+
coverage: string;
|
|
2304
|
+
/** Nothing was read: label the answer. */
|
|
2305
|
+
withoutSources: boolean;
|
|
2306
|
+
/** Sections were sent and the answer cites none: "No sources cited". */
|
|
2307
|
+
noSources: boolean;
|
|
2308
|
+
/** Navigation calls made (a repair turn is not a hop). */
|
|
2309
|
+
hops: number;
|
|
2310
|
+
droppedCount: number;
|
|
2311
|
+
secretsLeftOut: number;
|
|
2312
|
+
/** Nav entries the scan left out of the prompts. */
|
|
2313
|
+
entriesLeftOut: number;
|
|
2314
|
+
run: DocsRunOutcome;
|
|
2315
|
+
}
|
|
2316
|
+
type DocsTurnOutcome = DocsTurnAnswer
|
|
2317
|
+
/** S6: a cloud route (or none) and a base that may not leave the machine. Nothing was asked or sent. */
|
|
2318
|
+
| {
|
|
2319
|
+
kind: 'baseLocalOnly';
|
|
2320
|
+
bases: Array<{
|
|
2321
|
+
id: string;
|
|
2322
|
+
name: string;
|
|
2323
|
+
}>;
|
|
2324
|
+
route: AiRouteOk | null;
|
|
2325
|
+
} | {
|
|
2326
|
+
kind: 'noActiveBases';
|
|
2327
|
+
}
|
|
2328
|
+
/** The question itself looks like it holds a key. Nothing was sent. */
|
|
2329
|
+
| {
|
|
2330
|
+
kind: 'questionHasSecret';
|
|
2331
|
+
}
|
|
2332
|
+
/** Another request of this editor is in flight. */
|
|
2333
|
+
| {
|
|
2334
|
+
kind: 'busy';
|
|
2335
|
+
} | {
|
|
2336
|
+
kind: 'cancelled';
|
|
2337
|
+
} | {
|
|
2338
|
+
kind: 'error';
|
|
2339
|
+
error: AiError;
|
|
2340
|
+
};
|
|
2341
|
+
/** "Active" (A1): enabled and with a root found. Host-reported state, re-checked rather than assumed. */
|
|
2342
|
+
declare function docsActiveBases(statuses: readonly AiDocBaseStatus[]): AiDocBaseStatus[];
|
|
2343
|
+
/** S6: the active bases `route` may not carry. A null route counts as not local (fail closed). */
|
|
2344
|
+
declare function docsBlockingBases(active: readonly AiDocBaseStatus[], route: AiRouteOk | null): AiDocBaseStatus[];
|
|
2345
|
+
/**
|
|
2346
|
+
* The bases of this turn that may not go on to the answer call, by what the
|
|
2347
|
+
* host says now (F6): one that was disabled or removed mid-turn on any route,
|
|
2348
|
+
* one whose cloud switch went off on a route that is not local.
|
|
2349
|
+
*/
|
|
2350
|
+
declare function docsBasesOffMidTurn(active: readonly AiDocBaseStatus[], now: readonly AiDocBaseStatus[], route: AiRouteOk | null): Array<{
|
|
2351
|
+
id: string;
|
|
2352
|
+
name: string;
|
|
2353
|
+
}>;
|
|
2354
|
+
/**
|
|
2355
|
+
* One turn, start to finish. Never rejects: a cancelled or failed hop ends the
|
|
2356
|
+
* turn with an outcome, and what Activity recorded stays there.
|
|
2357
|
+
*/
|
|
2358
|
+
declare function runDocsTurn(deps: DocsLoopDeps, input: DocsTurnInput, events: DocsTurnEvents, signal: AbortSignal): Promise<DocsTurnOutcome>;
|
|
2359
|
+
interface DocsChat {
|
|
2360
|
+
/** A turn is in flight. */
|
|
2361
|
+
readonly busy: boolean;
|
|
2362
|
+
/** Start a turn; `{ kind: 'busy' }`, with nothing touched, while another is running. Abort `signal` to cancel the hops and the answer. */
|
|
2363
|
+
run(input: DocsTurnInput, events: DocsTurnEvents, signal: AbortSignal): Promise<DocsTurnOutcome>;
|
|
2364
|
+
}
|
|
2365
|
+
/** One per editor: one chat request in flight at a time (decision 13). */
|
|
2366
|
+
declare function createDocsChat(deps: DocsLoopDeps): DocsChat;
|
|
2367
|
+
|
|
2368
|
+
/**
|
|
2369
|
+
* The navigation hop's prompt: outlines and links only, never a body (A5),
|
|
2370
|
+
* and the parser for what comes back. Pure.
|
|
2371
|
+
*
|
|
2372
|
+
* Everything between `<index>` and `</index>` came from someone's files, so
|
|
2373
|
+
* the prompt says it is data (S10). The loop scans every string for secrets
|
|
2374
|
+
* (S7, A6) before it gets here. Nothing in a reply is trusted: `read` is
|
|
2375
|
+
* returned as it arrived and the loop filters it through the index (S1).
|
|
2376
|
+
*/
|
|
2377
|
+
|
|
2378
|
+
/** Output budget for one hop: a short list of ids (`runAiCompletion` adds reasoning headroom when thinking is on). */
|
|
2379
|
+
declare const DOCS_NAV_MAX_TOKENS = 768;
|
|
2380
|
+
/** One thing the model can ask for. `id` null: a label over `children`. */
|
|
2381
|
+
interface DocsNavEntry {
|
|
2382
|
+
id: string | null;
|
|
2383
|
+
text: string;
|
|
2384
|
+
children?: DocsNavEntry[];
|
|
2385
|
+
}
|
|
2386
|
+
/** A page's or a root's part of the index. */
|
|
2387
|
+
interface DocsNavBlock {
|
|
2388
|
+
/** "Root docs, home page: Handbook". */
|
|
2389
|
+
label: string;
|
|
2390
|
+
outline: DocsNavEntry[];
|
|
2391
|
+
links: DocsNavEntry[];
|
|
2392
|
+
/** Entries a cap already kept out of this block. */
|
|
2393
|
+
omitted: number;
|
|
2394
|
+
}
|
|
2395
|
+
interface DocsNavigatePromptInput {
|
|
2396
|
+
question: string;
|
|
2397
|
+
blocks: readonly DocsNavBlock[];
|
|
2398
|
+
/** Offered ids already asked for, so the model moves on. */
|
|
2399
|
+
alreadyRead: readonly string[];
|
|
2400
|
+
hop: number;
|
|
2401
|
+
maxHops: number;
|
|
2402
|
+
navigateBy: AiDocsNavigateBy;
|
|
2403
|
+
/** Characters of index text allowed. */
|
|
2404
|
+
maxChars: number;
|
|
2405
|
+
}
|
|
2406
|
+
interface DocsNavigatePrompt {
|
|
2407
|
+
messages: AiMessage[];
|
|
2408
|
+
/** Ids the index text offers. Zero means there is nothing to ask for: send nothing. */
|
|
2409
|
+
offered: number;
|
|
2410
|
+
/** Entries that did not fit, or that a cap kept out. */
|
|
2411
|
+
omitted: number;
|
|
2412
|
+
}
|
|
2413
|
+
declare function docsNavigateSystemPrompt(navigateBy: AiDocsNavigateBy): string;
|
|
2414
|
+
/** One line of index text: no way to start a new line or run on. Call it after the secrets scan, never before. */
|
|
2415
|
+
declare function docsNavText(text: string, max?: number): string;
|
|
2416
|
+
declare function buildDocsNavigateMessages(input: DocsNavigatePromptInput): DocsNavigatePrompt;
|
|
2417
|
+
type DocsNavReply = {
|
|
2418
|
+
ok: true;
|
|
2419
|
+
read: unknown[];
|
|
2420
|
+
done: boolean;
|
|
2421
|
+
} | {
|
|
2422
|
+
ok: false;
|
|
2423
|
+
error: string;
|
|
2424
|
+
};
|
|
2425
|
+
/**
|
|
2426
|
+
* A navigation reply's shape, and nothing more: `read` comes back as it
|
|
2427
|
+
* arrived (ids, numbers, whatever) for the loop to filter. `done` counts only
|
|
2428
|
+
* when it is exactly `true`. A bare array is taken as `read`. Missing `read`
|
|
2429
|
+
* is fine with `done: true`; otherwise it is a parse failure and gets the one
|
|
2430
|
+
* repair turn, as does a `read` that is not an array.
|
|
2431
|
+
*/
|
|
2432
|
+
declare function parseDocsNavReply(raw: string): DocsNavReply;
|
|
2433
|
+
|
|
2434
|
+
/**
|
|
2435
|
+
* The answer call's prompt: the question, the coverage note and the numbered
|
|
2436
|
+
* sections, delimited and declared to be data. Pure. Only the sections that
|
|
2437
|
+
* passed the secrets scan and the budget ever reach this builder, so the
|
|
2438
|
+
* Activity log, which records messages verbatim, cannot see the others (A7).
|
|
2439
|
+
*
|
|
2440
|
+
* The answer is text for a person. It is never parsed for further reads.
|
|
2441
|
+
*/
|
|
2442
|
+
|
|
2443
|
+
/** The longest answer asked for; a small context gets less (`docsAnswerMaxTokens`). */
|
|
2444
|
+
declare const DOCS_ANSWER_MAX_TOKENS = 2048;
|
|
2445
|
+
interface DocsAnswerPromptInput {
|
|
2446
|
+
question: string;
|
|
2447
|
+
sent: readonly AiDocsNumberedSection[];
|
|
2448
|
+
/** "answered from 4 of 7 sections, 3 left out". Counts only, no page text. */
|
|
2449
|
+
coverage: string | null;
|
|
2450
|
+
scope: 'bases' | 'document';
|
|
2451
|
+
}
|
|
2452
|
+
declare function docsAnswerSystemPrompt(scope: DocsAnswerPromptInput['scope'], withSources: boolean): string;
|
|
2453
|
+
declare function buildDocsAnswerMessages(input: DocsAnswerPromptInput): AiMessage[];
|
|
2454
|
+
/** Answer budget: what the context has left after the prompt, within a range (`runAiCompletion` adds reasoning headroom when thinking is on). */
|
|
2455
|
+
declare function docsAnswerMaxTokens(contextTokens: number, messages: readonly AiMessage[]): number;
|
|
2456
|
+
/** The answer without reasoning blocks: what the panel streams and stores. */
|
|
2457
|
+
declare function finalizeDocsAnswer(raw: string): string;
|
|
2458
|
+
|
|
2459
|
+
/**
|
|
2460
|
+
* Privacy copy for cloud engines: the four lines every surface shows (the
|
|
2461
|
+
* privacy card, the disclaimer, the consent dialogs), what each feature
|
|
2462
|
+
* sends, each provider's policy link, and the consent dialog text both hosts
|
|
2463
|
+
* render (VS Code's modal, Desktop's native dialog; Desktop has a Rust port
|
|
2464
|
+
* checked against `test/ai-fixtures/consent/`).
|
|
2465
|
+
*
|
|
2466
|
+
* No claims about any provider's training or retention: those are set by the
|
|
2467
|
+
* provider's terms and change. The copy says "may" and points at the policy.
|
|
2468
|
+
*/
|
|
2469
|
+
|
|
2470
|
+
/** The four lines of the privacy card, in display order. */
|
|
2471
|
+
declare const AI_CLOUD_PRIVACY: {
|
|
2472
|
+
readonly leaves: "Text you ask about is sent over the internet to the provider and processed on their servers.";
|
|
2473
|
+
readonly terms: string;
|
|
2474
|
+
readonly local: "A local engine keeps everything on this machine or your network.";
|
|
2475
|
+
readonly keys: "Your API key stays in this app's secure storage on this machine.";
|
|
2476
|
+
};
|
|
2477
|
+
/**
|
|
2478
|
+
* The privacy card's lines for an engine built into the host (VS Code's
|
|
2479
|
+
* Copilot models, `vscode-lm`): no API key, and the text goes through the
|
|
2480
|
+
* host to GitHub's Copilot service under the user's Copilot plan. No claim
|
|
2481
|
+
* about how a plan handles it; the policy link says.
|
|
2482
|
+
*/
|
|
2483
|
+
declare const AI_HOST_PRIVACY: {
|
|
2484
|
+
readonly leaves: "Text you ask about goes through VS Code to GitHub’s Copilot service and is processed on GitHub’s servers, under your Copilot plan.";
|
|
2485
|
+
readonly terms: "Whether it is retained or used for training is set by your plan’s terms, which differ between individual and business plans and may change; read them before you decide.";
|
|
2486
|
+
readonly local: "A local engine keeps everything on this machine or your network.";
|
|
2487
|
+
readonly permission: "VS Code also asks for its own permission the first time MikeDown uses Copilot.";
|
|
2488
|
+
};
|
|
2489
|
+
/**
|
|
2490
|
+
* The sentence about the consent step, for the cloud privacy card: whether
|
|
2491
|
+
* the host asks before a cloud engine's first request (`hostAsks`: it has
|
|
2492
|
+
* `requestCloudConsent`) or sends without asking.
|
|
2493
|
+
*/
|
|
2494
|
+
declare function aiConsentStepNote(hostAsks: boolean): string;
|
|
2495
|
+
/** What a request of each scope sends, as the object of "sends …". */
|
|
2496
|
+
declare const AI_SENDS_TEXT: Readonly<Record<AiSendsScope, string>>;
|
|
2497
|
+
/** For a request whose feature is unknown. */
|
|
2498
|
+
declare const AI_SENDS_FALLBACK = "the text you ask the AI about";
|
|
2499
|
+
/** Most base names a consent sentence lists before it says "and N more". */
|
|
2500
|
+
declare const AI_SENDS_MAX_BASE_NAMES = 5;
|
|
2501
|
+
/**
|
|
2502
|
+
* What a request sends, as the object of a sentence ("MikeDown sends …").
|
|
2503
|
+
* `sends` wins (whole-document Translate runs as the `rewrite` feature);
|
|
2504
|
+
* else the feature's own scope; else a generic phrase. For `docsBase`,
|
|
2505
|
+
* `baseNames` (the host's names for the bases the request would read from)
|
|
2506
|
+
* are spelled out: "pages from the documentation bases you turned on:
|
|
2507
|
+
* “Handbook” and “Notes”". Without names it is the bare phrase.
|
|
2508
|
+
*/
|
|
2509
|
+
declare function whatIsSent(feature?: string, sends?: AiSendsScope, baseNames?: readonly string[]): string;
|
|
2510
|
+
/** The policy of the cloud preset on `host` (an engine's base URL host), if one matches. */
|
|
2511
|
+
declare function aiProviderPolicyForHost(host: string | null | undefined, presets?: readonly AiEnginePreset[]): AiProviderPolicy | undefined;
|
|
2512
|
+
/**
|
|
2513
|
+
* The policy for an engine, matched by its base URL's host (engines do not
|
|
2514
|
+
* store a preset); a host kind with no address (`vscode-lm`) by its kind.
|
|
2515
|
+
*/
|
|
2516
|
+
declare function aiProviderPolicy(engine: {
|
|
2517
|
+
baseUrl: string;
|
|
2518
|
+
kind?: string;
|
|
2519
|
+
}, presets?: readonly AiEnginePreset[]): AiProviderPolicy | undefined;
|
|
2520
|
+
/**
|
|
2521
|
+
* Where an engine sends text, as the consent dialog's `host`: the base URL's
|
|
2522
|
+
* host ("api.anthropic.com"), the URL itself when it has none, or the
|
|
2523
|
+
* service's name for a host kind ("GitHub Copilot" for `vscode-lm`).
|
|
2524
|
+
*/
|
|
2525
|
+
declare function aiConsentHost(engine: {
|
|
2526
|
+
baseUrl: string;
|
|
2527
|
+
kind?: string;
|
|
2528
|
+
}): string;
|
|
2529
|
+
/** Where a consent answer is remembered: VS Code says workspace, Desktop folder (or session without one). */
|
|
2530
|
+
type AiConsentScopeWord = 'workspace' | 'folder' | 'session';
|
|
2531
|
+
interface AiConsentTextInput {
|
|
2532
|
+
engineName: string;
|
|
2533
|
+
/** The engine's kind; a host kind (`vscode-lm`) reads as a service built into VS Code, not an address. */
|
|
2534
|
+
kind?: string;
|
|
2535
|
+
/** The engine's host ("api.anthropic.com"), or its base URL when it has none. */
|
|
2536
|
+
host: string;
|
|
2537
|
+
feature?: string;
|
|
2538
|
+
sends?: AiSendsScope;
|
|
2539
|
+
/** Names of the bases a `docsBase` request reads from, as the host knows them (never from the webview). */
|
|
2540
|
+
baseNames?: readonly string[];
|
|
2541
|
+
scopeWord: AiConsentScopeWord;
|
|
2542
|
+
/** The provider's policy (see `aiProviderPolicyForHost`). */
|
|
2543
|
+
policy?: AiProviderPolicy | null;
|
|
2544
|
+
/**
|
|
2545
|
+
* Spell the policy link out in the body, for a dialog that can neither
|
|
2546
|
+
* link nor spare a button for it (Desktop's native dialog). VS Code's modal
|
|
2547
|
+
* has a "Read the provider's policy" button instead.
|
|
2548
|
+
*/
|
|
2549
|
+
policyLine?: boolean;
|
|
2550
|
+
}
|
|
2551
|
+
interface AiConsentText {
|
|
2552
|
+
title: string;
|
|
2553
|
+
/** Paragraphs separated by a blank line. */
|
|
2554
|
+
body: string;
|
|
2555
|
+
/** Remembered for the workspace or folder (for the session without one). */
|
|
2556
|
+
allow: string;
|
|
2557
|
+
/** This request only; nothing is remembered and the next request asks again. */
|
|
2558
|
+
allowOnce: string;
|
|
2559
|
+
/** Declines; the host does not ask about this engine again until it restarts. */
|
|
2560
|
+
deny: string;
|
|
2561
|
+
/** The policy button's label, when there is a policy and the body does not spell it out. */
|
|
2562
|
+
readPolicy?: string;
|
|
2563
|
+
}
|
|
2564
|
+
/** The consent dialog's words, the single source for both hosts. */
|
|
2565
|
+
declare function aiConsentText(input: AiConsentTextInput): AiConsentText;
|
|
2566
|
+
/** Shown in the Cloud consents list when there is nothing stored. */
|
|
2567
|
+
declare const AI_NO_CONSENTS_TEXT = "No cloud engine has been allowed here.";
|
|
2568
|
+
/**
|
|
2569
|
+
* One line of the Cloud consents list, without the Forget button:
|
|
2570
|
+
* "Anthropic · this workspace · Allowed". A stored "never" (older versions)
|
|
2571
|
+
* reads "Not allowed"; this session's "Don't allow", "Not allowed until restart".
|
|
2572
|
+
*/
|
|
2573
|
+
declare function aiConsentSummary(record: Pick<AiConsentRecord, 'answer' | 'scope'>, engineName: string): string;
|
|
2574
|
+
|
|
2575
|
+
/**
|
|
2576
|
+
* The setup wizard's first decision, as plain data: which path to offer
|
|
2577
|
+
* first (a running local server, VS Code's Copilot models, or a cloud
|
|
2578
|
+
* provider), whether this machine can run models locally, and how big a
|
|
2579
|
+
* model fits (OQ11, OQ13). No DOM, no network: the host reports the
|
|
2580
|
+
* hardware (`hardwareInfo`), the running servers (`detectLocalServers`) and
|
|
2581
|
+
* its built-in models (`detectHostEngines`).
|
|
2582
|
+
*
|
|
2583
|
+
* Every size here is a hint for a 4-bit quantised model, never a gate: the
|
|
2584
|
+
* user can pick any model.
|
|
2585
|
+
*/
|
|
2586
|
+
|
|
2587
|
+
/** OQ11: Apple Silicon with this much unified memory can run local models. */
|
|
2588
|
+
declare const AI_LOCAL_MIN_UNIFIED_GB = 16;
|
|
2589
|
+
/** OQ11: any machine with this much memory can. */
|
|
2590
|
+
declare const AI_LOCAL_MIN_MEMORY_GB = 32;
|
|
2591
|
+
/** OQ11: or an NVIDIA GPU with this much video memory (Desktop only, where it can be read). */
|
|
2592
|
+
declare const AI_LOCAL_MIN_VRAM_GB = 8;
|
|
2593
|
+
type AiSetupPath = 'runningLocal' | 'vscodeLm' | 'cloud';
|
|
2594
|
+
interface AiSetupInput {
|
|
2595
|
+
/** `detectLocalServers` results (every candidate, running or not). */
|
|
2596
|
+
running: readonly AiLocalServerProbe[];
|
|
2597
|
+
/** `hardwareInfo()`, or null when the host cannot tell. */
|
|
2598
|
+
hardware: AiHardwareInfo | null;
|
|
2599
|
+
/** VS Code only: whether `vscode.lm` offers a Copilot model (see `vscodeLmAvailability`). */
|
|
2600
|
+
vscodeLm?: {
|
|
2601
|
+
available: boolean;
|
|
2602
|
+
models: number;
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
interface AiSetupRecommendation {
|
|
2606
|
+
/** What the wizard offers first. */
|
|
2607
|
+
path: AiSetupPath;
|
|
2608
|
+
/** OQ11: this machine is above the threshold, so local is shown as an option (still under Advanced unless a server runs). */
|
|
2609
|
+
localCapable: boolean;
|
|
2610
|
+
/** The model-size sentence for this machine (OQ11); empty without hardware info. */
|
|
2611
|
+
localHint: string;
|
|
2612
|
+
/**
|
|
2613
|
+
* Step 1's hardware line: "Local is an option on this machine: Apple
|
|
2614
|
+
* Silicon, 32 GB. Models up to about 30B parameters run well." when
|
|
2615
|
+
* capable, the small-machine sentence when not, empty without hardware
|
|
2616
|
+
* info.
|
|
2617
|
+
*/
|
|
2618
|
+
hardwareLine: string;
|
|
2619
|
+
/** `runningLocal`: the server to offer (most models first, then preset order). */
|
|
2620
|
+
running?: AiLocalServerProbe;
|
|
2621
|
+
/** Why, one short sentence per signal checked, in order (for tooltips, logs and tests). */
|
|
2622
|
+
reasons: string[];
|
|
2623
|
+
}
|
|
2624
|
+
/** Bytes to gigabytes, for display ("32 GB"). */
|
|
2625
|
+
declare function formatMemoryGb(bytes: number): string;
|
|
2626
|
+
/** OQ11's threshold: Apple Silicon with 16 GB or more, any machine with 32 GB or more, or an NVIDIA GPU with 8 GB of VRAM or more. */
|
|
2627
|
+
declare function isLocalCapable(hw: AiHardwareInfo | null): boolean;
|
|
2628
|
+
/**
|
|
2629
|
+
* The memory a local model runs in: unified memory on Apple Silicon, an
|
|
2630
|
+
* NVIDIA card's VRAM when the host read it, else system memory (CPU
|
|
2631
|
+
* inference: slower, but it runs).
|
|
2632
|
+
*/
|
|
2633
|
+
declare function modelMemoryBytes(hw: AiHardwareInfo): number;
|
|
2634
|
+
/** OQ11's model-size sentence for a machine ("Models up to about 30B parameters run well."); empty for null. */
|
|
2635
|
+
declare function localSizeHint(hw: AiHardwareInfo | null): string;
|
|
2636
|
+
/** "Apple Silicon, 32 GB", "64 GB of memory, NVIDIA GeForce RTX 4090 (24 GB)", "16 GB of memory". */
|
|
2637
|
+
declare function describeHardware(hw: AiHardwareInfo): string;
|
|
2638
|
+
/** Shown on a machine below the OQ11 threshold. */
|
|
2639
|
+
declare const AI_LOCAL_SMALL_MACHINE = "This machine is on the small side for local models; cloud is the easier path.";
|
|
2640
|
+
/** Added when the host read no GPU (VS Code reads none): the hint comes from memory alone. */
|
|
2641
|
+
declare const AI_LOCAL_ROUGH_GUESS = "A rough guess from memory alone; the graphics card was not checked.";
|
|
2642
|
+
/**
|
|
2643
|
+
* The wizard's path, checked in order (OQ13):
|
|
2644
|
+
*
|
|
2645
|
+
* a. a local server is running and lists models → offer it first;
|
|
2646
|
+
* b. VS Code offers a Copilot model (`vscode-lm`, OQ12) → offer it first;
|
|
2647
|
+
* c. this machine is above the OQ11 threshold → cloud first, with "Local is
|
|
2648
|
+
* an option on this machine" shown;
|
|
2649
|
+
* d. otherwise cloud, with local under Advanced and no hint.
|
|
2650
|
+
*
|
|
2651
|
+
* `localCapable`, `localHint` and `hardwareLine` are filled whatever the path.
|
|
2652
|
+
*/
|
|
2653
|
+
declare function recommendSetupPath(input: AiSetupInput): AiSetupRecommendation;
|
|
2654
|
+
/** The recommendation's `vscodeLm` input from `detectHostEngines` results. */
|
|
2655
|
+
declare function vscodeLmAvailability(probes: readonly AiHostEngineProbe[] | null | undefined): {
|
|
2656
|
+
available: boolean;
|
|
2657
|
+
models: number;
|
|
2658
|
+
};
|
|
2659
|
+
/**
|
|
2660
|
+
* A model's parameter count in billions, from its id: `7b`, `30b`, `1.5b`,
|
|
2661
|
+
* `270m`, a mixture `8x7b` (total). The first count wins, so `30b-a3b`
|
|
2662
|
+
* (active parameters) reads 30. Null when the id names none.
|
|
2663
|
+
*/
|
|
2664
|
+
declare function modelParamsBillions(id: string): number | null;
|
|
2665
|
+
type AiModelSizeTag = 'fits' | 'tight' | 'too large';
|
|
2666
|
+
/**
|
|
2667
|
+
* "fits", "tight" or "too large" for a model on this machine: from the size
|
|
2668
|
+
* the server reported when there is one (`modelSizeHintFromBytes`), else the
|
|
2669
|
+
* parameter count in its id (OQ11's bands, 4-bit). Undefined when neither is
|
|
2670
|
+
* known or the hardware is unknown: no tag. A hint only; nothing is blocked.
|
|
2671
|
+
*/
|
|
2672
|
+
declare function modelSizeHint(id: string, hardware: AiHardwareInfo | null, sizeBytes?: number): AiModelSizeTag | undefined;
|
|
2673
|
+
/**
|
|
2674
|
+
* "fits", "tight" or "too large" from a size the server reported (bytes on
|
|
2675
|
+
* disk or its load estimate), against this machine's model memory.
|
|
2676
|
+
*/
|
|
2677
|
+
declare function modelSizeHintFromBytes(sizeBytes: number, hardware: AiHardwareInfo): AiModelSizeTag;
|
|
2678
|
+
/** A model size for a picker row: "16 GB", "2.6 GB", "0.2 GB". */
|
|
2679
|
+
declare function formatModelSize(bytes: number): string;
|
|
2680
|
+
/**
|
|
2681
|
+
* The active parameters (billions) an id names, as mixture-of-experts models
|
|
2682
|
+
* do: `Qwen3.6-35B-A3B`, `qwen3.5-122b-a10b`, `Nemotron-3-Super-120B-A12B`.
|
|
2683
|
+
* Null when the id names none.
|
|
2684
|
+
*/
|
|
2685
|
+
declare function modelActiveParamsBillions(id: string): number | null;
|
|
2686
|
+
/**
|
|
2687
|
+
* Whether a model is a mixture of experts, from its id's active parameters or
|
|
2688
|
+
* the server's word for it (`moe`: LM Studio's `arch`, oMLX's
|
|
2689
|
+
* `config_model_type`). Only a few billion parameters run per token, so it is
|
|
2690
|
+
* much faster than its size suggests. A row hint only, never a reason to pick it.
|
|
2691
|
+
*/
|
|
2692
|
+
declare function modelMoeHint(id: string, moe?: boolean): {
|
|
2693
|
+
activeBillions: number | null;
|
|
2694
|
+
} | null;
|
|
2695
|
+
/**
|
|
2696
|
+
* The picker's "MoE · fast" hint: a mixture of experts that also fits this
|
|
2697
|
+
* machine (`fits`). A tight fit (a 409 GB model on a 512 GB Mac) or one too
|
|
2698
|
+
* large runs slowly however few parameters work per token, and an unknown fit
|
|
2699
|
+
* proves nothing, so neither gets the speed hint (Q5).
|
|
2700
|
+
*/
|
|
2701
|
+
declare function modelMoeFastHint(id: string, moe: boolean | undefined, size: AiModelSizeTag | undefined): {
|
|
2702
|
+
activeBillions: number | null;
|
|
2703
|
+
} | null;
|
|
2704
|
+
/** One model as `localDefaultModel` weighs it. */
|
|
2705
|
+
interface AiLocalModelChoice {
|
|
2706
|
+
id: string;
|
|
2707
|
+
/** Can chat (an `llm` or `vlm`; not embeddings, speech or a converter). */
|
|
2708
|
+
chat: boolean;
|
|
2709
|
+
missing?: boolean;
|
|
2710
|
+
/** Loaded on the server right now. */
|
|
2711
|
+
loaded?: boolean;
|
|
2712
|
+
}
|
|
2713
|
+
/**
|
|
2714
|
+
* The model a new local engine starts with: a chat model the server has
|
|
2715
|
+
* loaded, else null and the user picks. Size is a poor guide to speed (a
|
|
2716
|
+
* mixture of experts runs far faster than a dense model its size), so no
|
|
2717
|
+
* guess is made from it.
|
|
2718
|
+
*/
|
|
2719
|
+
declare function localDefaultModel(models: readonly AiLocalModelChoice[]): string | null;
|
|
2720
|
+
|
|
2721
|
+
export { AI_AUDIENCES, AI_CHARS_PER_TOKEN, AI_CLOUD_PRIVACY, AI_ENGINE_PRESETS, AI_ERROR_COPY, AI_FEATURES, AI_HOST_PRESET_FAMILY, AI_HOST_PRIVACY, AI_LOCAL_DETECT_TIMEOUT_MS, AI_LOCAL_MIN_MEMORY_GB, AI_LOCAL_MIN_UNIFIED_GB, AI_LOCAL_MIN_VRAM_GB, AI_LOCAL_ROUGH_GUESS, AI_LOCAL_SMALL_MACHINE, AI_NO_CONSENTS_TEXT, AI_PRESET_FAMILIES, AI_REWRITE_VARIANTS, AI_SENDS_FALLBACK, AI_SENDS_MAX_BASE_NAMES, AI_SENDS_TEXT, AI_TASKS, ANSWERED_WITHOUT_SOURCES, ANTHROPIC_API_VERSION, ANTHROPIC_DEFAULT_MAX_TOKENS, ANTHROPIC_MODEL_TABLE, AiApiKeyFile, AiCapabilityFlag, AiCompletionRequest, AiCompletionResult, AiConsentRecord, AiDiscoveredModel, AiDocBaseStatus, AiDocsHome, AiDocsIndex, AiDocsLink, AiDocsNavigateBy, AiDocsOptions, AiDocsPage, AiDocsSection, AiDocsSectionRef, AiEngineConfig, AiEngineKind, AiEngineStatus, AiError, AiErrorCode, AiFeatureSettings, AiFinishReason, AiHardwareInfo, AiHostEngineProbe, AiLocalServerProbe, AiLocality, AiMessage, AiModelCapabilities, AiModelConfig, AiSendsScope, AiSettings, AiUsage, CHARS_PER_TOKEN, CONTINUE_DEFAULT_MAX_TOKENS, CONTINUE_MAX_MAX_TOKENS, CONTINUE_MIN_MAX_TOKENS, CONTINUE_SYSTEM_PROMPT, DEFAULT_LOCAL_NETWORKS, DOCS_ANSWER_MAX_TOKENS, DOCS_BUILD_BUDGET_MS, DOCS_CHAT_FEATURE, DOCS_DOCUMENT_BASE_ID, DOCS_KEYWORD_MAX_TERMS, DOCS_KEYWORD_TOP, DOCS_LINK_SCAN_UNITS, DOCS_LINK_TEXT_UNITS, DOCS_MAP_HEADINGS_PER_PAGE, DOCS_MAX_ACTIVITY_IDS, DOCS_MAX_ACTIVITY_ID_CHARS, DOCS_MAX_DIRS, DOCS_MAX_FILES, DOCS_MAX_FILE_BYTES, DOCS_MAX_HEADINGS_PER_PAGE, DOCS_MAX_HEADING_CHARS, DOCS_MAX_ID_CHARS, DOCS_MAX_LINKS, DOCS_MAX_LINKS_PER_PAGE, DOCS_MAX_LINK_TARGET_CHARS, DOCS_MAX_LINK_WORK, DOCS_MAX_MAP_ENTRIES, DOCS_MAX_NAV_CHARS, DOCS_MAX_READ_IDS_PER_HOP, DOCS_MAX_SECTION_BYTES, DOCS_NAV_MAX_TOKENS, DOCS_NO_HOME_TEXT, DOCS_STOP_WORDS, EXPLAIN_MAX_TOKENS, EXPLAIN_SYSTEM_ADDENDUM, EXPLAIN_SYSTEM_PROMPT, GHOST_AFTER_CHARS, GHOST_BEFORE_CHARS, GHOST_DEFAULT_CONTEXT_TOKENS, GHOST_OUTLINE_MAX_CHARS, GHOST_OUTLINE_MAX_ITEMS, GHOST_REPEAT_CHARS, GHOST_RESERVE_TOKENS, GHOST_SENTENCE_CUT_CHARS, JSON_ONLY_INSTRUCTION, JSON_SHAPE_HINT, MODEL_LOAD_FAILED_PHRASES, MikeDownDocsHost, NO_SOURCES_CITED, OPENAI_MODEL_TABLE, OUTLINE_SYSTEM_PROMPT, PROPERTIES_SYSTEM_PROMPT, PROPERTY_DESCRIPTION_MAX_CHARS, PROPERTY_MAX_TAGS, PROPERTY_TITLE_MAX_CHARS, PROPERTY_VOCABULARY_MAX, RETRYABLE_REQUEST_PARAMS, REWRITE_CONTEXT_CHARS, REWRITE_INSTRUCTION_MAX_CHARS, REWRITE_LANGUAGE_MAX_CHARS, REWRITE_SYSTEM_PROMPT, STREAM_USAGE_HOSTS, SUMMARIZE_MIN_CONTEXT_TOKENS, SUMMARY_SHORT_DOCUMENT_CHARS, SUMMARY_SYSTEM_PROMPT, SseParser, THINKING_DEFAULT_HEADROOM_TOKENS, THINKING_HEADROOM_TOKENS, THINKING_MAX_TOKENS_CAP, TITLE_MAX_CHARS, TITLE_OUTLINE_MAX_ITEMS, TITLE_SYSTEM_PROMPT, TRANSLATE_DOCUMENT_NOTE, TRANSLATE_SYSTEM_ADDENDUM, aiConsentHost, aiConsentStepNote, aiConsentSummary, aiConsentText, aiErrorCodeFromHttp, aiErrorCopy, aiLengthLimitCopy, aiProviderPolicy, aiProviderPolicyForHost, aiResultStats, aiResultStatsText, aiResultStatsTitle, aiTimingLogParts, anthropicNextPage, base64ToBytes, budgetDocsSections, budgetDocument, buildAuthHeaders, buildCompletionBody, buildContinueMessages, buildDocsAnswerMessages, buildDocsIndex, buildDocsMap, buildDocsNavigateMessages, buildEmbeddingsBody, buildExplainMessages, buildPropertiesMessages, buildRewriteMessages, buildSummaryMessages, buildTitleMessages, bytesToBase64, capSectionText, checkMarkdownStructure, chooseDocsHomes, citationLink, clampDocsHops, clampDocsSourceShare, clipCodePoints, clipDocsHeading, cmpCodePoints, combineRequirements, completeCapabilities, containsSecret, continueMaxTokens, countDetectedModels, countMarkdownStructure, countOccurrences, coveragePercent, createAnthropicStreamReader, createCompletionStreamReader, createDocsChat, createDocsLinkResolver, createOpenAiStreamReader, defaultAiDocsOptions, defaultAiSettings, describeHardware, detectCapabilities, detectLocality, docsActiveBases, docsAnswerMaxTokens, docsAnswerSystemPrompt, docsBasesOffMidTurn, docsBlockingBases, docsBudgetChars, docsCitationLabel, docsCoverageNote, docsDocumentLocation, docsFileHref, docsFileStem, docsHasNoHome, docsHeadingAt, docsHomeHealth, docsIdRoot, docsLinkList, docsNavText, docsNavigateSystemPrompt, docsPageId, docsPagesById, docsRootLabels, docsSectionFile, docsSectionId, docsSectionSlug, docsSlug, docsSlugs, dropSecretSections, encodeLinkPath, engineEndpoints, engineLocality, escapeClosingTag, existingTagsOf, explainMaxTokens, finalizeDocsAnswer, finalizeRewriteText, finalizeSummary, findSecret, findSecretAtCutoff, foldAscii, formatAiSeconds, formatAiTokensPerSecond, formatMemoryGb, formatModelSize, fromWireCompletionRequest, frontmatterEndOffset, getAiAudience, getAiFeature, getAiRewriteVariant, getAiTask, ghostAcceptWord, ghostContext, ghostPrefixMatch, githubAnchors, headingsOutline, headingsWithOffsets, hostOfUrl, insertUrlIsVerbatim, isAiError, isAiErrorCode, isLocalCapable, isOpenAiReasoningModel, isRouteUsable, isSafeRelPath, jsonRepairMessages, keywordTerms, localDefaultModel, localDetectCandidates, localDetectTarget, localDetectUrl, localSizeHint, localityMismatch, makeAiError, mergeDiscoveredModels, mergeRequestOverrides, missingCapabilities, modelActiveParamsBillions, modelMemoryBytes, modelMoeFastHint, modelMoeHint, modelParamsBillions, modelSizeHint, modelSizeHintFromBytes, nearestThinkingLevel, normalizeRewriteLanguage, openAiOffReasoningEffort, parseAnthropicModels, parseCitations, parseDocsId, parseDocsNavReply, parseEmbeddingsResponse, parseLenientJson, parseLmStudioModels, parseModelSizes, parseOllamaRunning, parseOllamaShow, parseOllamaTags, parseOmlxModelStatus, parseOpenAiModelList, parsePropertySuggestions, parseTitles, prepareInsertMarkdown, presetForEngine, propertiesMaxTokens, rankDocsWikilinkCandidates, rankKeywordSections, recommendSetupPath, relativeDocsPath, renderDocsMap, renderHeadingOutline, resolveAiFeature, resolveRoute, resolveThinkingLevel, rewriteCitationsForInsert, rewriteInstruction, rewriteMaxTokens, rewriteSystemPrompt, rewriteVisibleText, runDocsTurn, sanitizeInsertMarkdown, scanDocsSections, scanForSecrets, scoreKeywordSection, secretsLeftOutPhrase, splitDocsSections, streamUsageSupported, summaryMaxTokens, summaryPreviewText, summaryVisibleText, taskThinkingDefault, thinkingChoices, thinkingLevelFrom, thinkingMaxTokens, thinkingOverrides, titleMaxTokens, toAiError, toWireCompletionRequest, trimGhostSuggestion, trimGhostSuggestionDetailed, trimRewriteContext, unsupportedRequestParam, vscodeLmAvailability, whatIsSent, withModelSizes, withModelStatus, withStreamUsage, withThinkingLevel, withoutRequestParam };
|
|
2722
|
+
export type { AiAudience, AiAudienceDefinition, AiCapabilityRequirements, AiConsentScopeWord, AiConsentState, AiConsentText, AiConsentTextInput, AiDocsActivityDetail, AiDocsBrokenReason, AiDocsIndexInput, AiDocsLinkStatus, AiDocsNumberedSection, AiDocsPageInput, AiDocsResolvedLink, AiEngineEndpoints, AiEnginePreset, AiErrorCopy, AiFeatureDefinition, AiFrontmatterEntry, AiLocalDetectTarget, AiLocalModelChoice, AiModelSizeTag, AiPopularRule, AiPresetFamily, AiPresetFamilyInfo, AiProviderPolicy, AiResultStats, AiRewriteGroup, AiRewriteOutput, AiRewriteParams, AiRewriteScope, AiRewriteVariant, AiRewriteVariantDefinition, AiRouteOk, AiRoutePin, AiRouteResolution, AiRouteUnusable, AiSetupInput, AiSetupPath, AiSetupRecommendation, AiSummaryKind, AiTaskDefinition, AiThinkingChoice, AiThinkingJob, AiThinkingLevel, AiThinkingOptions, AiUnusableCode, AiWireCompletionRequest, AiWireMessage, AiWireMessagePart, BudgetedDocument, BuildCompletionOptions, CompletionStreamReader, ContinuePromptInput, DetectedCapabilities, DocsAnswerPromptInput, DocsBudgetOptions, DocsBudgetResult, DocsChat, DocsCitationPart, DocsDocumentLocation, DocsHeadingHit, DocsKeywordSection, DocsLinkList, DocsLinkResolver, DocsLoopDeps, DocsMap, DocsMapEntry, DocsMarkdownSection, DocsNavBlock, DocsNavEntry, DocsNavReply, DocsNavigatePrompt, DocsNavigatePromptInput, DocsReadHost, DocsRouteRequest, DocsRunEvents, DocsRunOutcome, DocsRunRequest, DocsRunThinking, DocsSecretKind, DocsSentSection, DocsTurnAnswer, DocsTurnEvents, DocsTurnInput, DocsTurnOutcome, DocsTurnProgress, DocsTurnRoute, DocsTurnScope, DocumentBudgetOptions, ExplainPromptInput, GhostBlockKind, GhostContext, GhostContextOptions, GhostTrimOptions, GhostTrimResult, HeadingOutlineEntry, HeadingWithOffsets, InsertUrlPolicy, LenientJsonResult, ParsedCitations, ParsedPropertySuggestions, ParsedTitles, PropertiesPromptInput, PropertySuggestions, ResolveRouteOptions, ResolvedAiFeature, ResolvedThinkingLevel, RewritePromptInput, SanitizedMarkdown, SecretScan, SseEvent, StreamOutcome, StructureCounts, StructureDiff, StructureMismatch, SuggestedTag, SummaryPromptInput, ThinkingBudgetOptions, TitlePromptInput };
|