harn-vm 0.10.162

Async bytecode virtual machine for the Harn programming language
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
//! Aggregate provider/model catalog document: `ProvidersConfig`, its overlay
//! merge semantics, field-wise `[patch.models]` application, and the
//! tier/inference rule DTOs.
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicBool, Ordering};

use serde::Deserialize;

use super::*;

#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
pub struct ProvidersConfig {
    #[serde(default)]
    pub default_provider: Option<String>,
    #[serde(default)]
    pub fallback_model: Option<String>,
    #[serde(default)]
    pub provider_defaults: BTreeMap<String, ProviderDefaultModels>,
    #[serde(default)]
    pub providers: BTreeMap<String, ProviderDef>,
    #[serde(default)]
    pub aliases: BTreeMap<String, AliasDef>,
    #[serde(default)]
    pub alias_tool_calling: BTreeMap<String, AliasToolCallingDef>,
    #[serde(default)]
    pub models: BTreeMap<String, ModelDef>,
    #[serde(default)]
    pub qc_defaults: BTreeMap<String, String>,
    #[serde(default)]
    pub inference_rules: Vec<InferenceRule>,
    #[serde(default)]
    pub tier_rules: Vec<TierRule>,
    #[serde(default)]
    pub tier_defaults: TierDefaults,
    #[serde(default)]
    pub model_defaults: BTreeMap<String, BTreeMap<String, toml::Value>>,
    #[serde(default)]
    pub model_roles: BTreeMap<String, BTreeMap<String, toml::Value>>,
    #[serde(default)]
    pub suppress: SuppressDef,
    #[serde(default)]
    pub patch: PatchDef,
    /// `[model_ladders.<name>]` tables: named model-fallback ladders that a
    /// `ladder: "<name>"` option on `llm_call` resolves and lowers onto the
    /// routing chain. Keyed by ladder name.
    #[serde(default)]
    pub model_ladders: BTreeMap<String, ModelLadderDef>,
    /// Host-facing model-selection metadata. This never changes routing by
    /// itself; it only describes how clients may present catalog choices.
    #[serde(default)]
    pub presentation: PresentationConfig,
    /// Source-only audit registry for provider usage-accounting behavior.
    /// The generated catalog projects the audited booleans, while this
    /// registry keeps evidence and the explicit unverified queue at their
    /// owning source seam without changing the public artifact schema.
    #[serde(default)]
    pub usage_accounting_audit: Option<UsageAccountingAuditRegistry>,
    /// Evidence and the expiring unresearched queue behind every provider's
    /// `data_controls` declaration. Providers are never merely missing from
    /// it: a provider is either researched or explicitly queued.
    #[serde(default)]
    pub data_controls_audit: Option<DataControlsAuditRegistry>,
    /// The posture Harn requests when a caller names none. Shipped as
    /// `default` on purpose — see [`DataPosture`]. An embedder that wants the
    /// strictest available posture everywhere flips this one key in config.
    #[serde(default)]
    pub data_controls_policy: DataControlsPolicy,
}

/// Product entry points may deliberately choose different defaults, but both
/// choices live beside the model rows they reference.
#[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq)]
pub struct ProviderDefaultModels {
    #[serde(default)]
    pub runtime: Option<String>,
    #[serde(default)]
    pub portal: Option<String>,
}

/// Field-wise catalog patches applied on top of merged model rows.
///
/// Overlays have three complementary tools for adjusting the baseline
/// catalog, from coarsest to finest:
///
/// 1. **Whole-row replace** — `[models.<id>]` replaces the entire model row.
///    Use it to add a new route or when the overlay intentionally owns every
///    field of the row.
/// 2. **Field patch** — `[patch.models.<id>]` merges individual fields into
///    the existing row, leaving every unmentioned field at its baseline
///    value. Use it to tweak one knob (a `stream_timeout`, one pricing rate)
///    without copying the row verbatim and silently freezing the rest of its
///    fields against upstream catalog updates.
/// 3. **Route suppression** — `[suppress]` hides baseline routes from the
///    exported/served artifact entirely (see [`SuppressDef`]).
///
/// Patch semantics:
/// - Nested tables merge recursively; scalars **and arrays** replace the
///   base value wholesale (there is deliberately no per-element array merge).
/// - Within a single overlay, `[models.<id>]` whole-row replacement applies
///   BEFORE `[patch.models.<id>]`, so patch fields win over the same
///   overlay's whole-row fields.
/// - Patches are STICKY across layers: once accumulated, a patch re-applies
///   after every later layer's merge, including a later layer's whole-row
///   replacement of the same id. A patch means "always tweak this field",
///   not "tweak it once".
/// - A patch whose target row does not exist yet stays in the accumulator
///   silently and applies as soon as a later layer contributes the row;
///   [`ProvidersConfig::dangling_model_patches`] reports the leftovers for
///   doctor/export validation.
/// - A patch that produces a type-invalid row warns once per process and
///   keeps the unpatched row.
#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
pub struct PatchDef {
    /// `[patch.models.<id>]` tables: partial `ModelDef` field sets merged
    /// field-wise into the model row with the same catalog id.
    #[serde(default)]
    pub models: BTreeMap<String, toml::Value>,
}

/// Routes hidden from the exported/served provider catalog artifact.
///
/// Lets an overlay drop baseline routes that are broken or unusable for the
/// embedding product (e.g. a dedicated-only serving route, or a local image
/// with a broken server-side tool parser) without forking the baseline
/// catalog. Suppression is artifact-level presentation: it removes the model
/// row, its aliases, and any recommendation variant derived from it, but does
/// not block runtime resolution of an explicitly requested model id.
///
/// This is one of three overlay tools (see [`PatchDef`] for the full set):
/// whole-row `[models.<id>]` replacement, field-wise `[patch.models.<id>]`
/// patches, and `[suppress]` route suppression. Combined with whole-row
/// `models` replacement, suppression also expresses route renames: define
/// the row under the new id and suppress the old one.
#[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq)]
pub struct SuppressDef {
    /// `"provider:model_id"` selectors. Split on the FIRST colon only —
    /// model ids may themselves contain colons (e.g. Ollama image tags such
    /// as `ollama:qwen3.6:35b-a3b-coding-nvfp4`). Entries without a colon
    /// match nothing.
    #[serde(default)]
    pub routes: Vec<String>,
}

impl ProvidersConfig {
    pub fn is_empty(&self) -> bool {
        self.default_provider.is_none()
            && self.fallback_model.is_none()
            && self.provider_defaults.is_empty()
            && self.providers.is_empty()
            && self.aliases.is_empty()
            && self.alias_tool_calling.is_empty()
            && self.models.is_empty()
            && self.qc_defaults.is_empty()
            && self.inference_rules.is_empty()
            && self.tier_rules.is_empty()
            && self.model_defaults.is_empty()
            && self.model_roles.is_empty()
            && self.suppress.routes.is_empty()
            && self.patch.models.is_empty()
            && self.model_ladders.is_empty()
            && self.presentation.is_empty()
            && self.usage_accounting_audit.is_none()
            && self.data_controls_audit.is_none()
            && self.data_controls_policy == DataControlsPolicy::default()
            && self.tier_defaults.default == default_mid()
    }

    /// `[patch.models]` ids with no matching model row in the merged config.
    ///
    /// Dangling patches are not an error at merge time — the row may arrive
    /// from a later layer — but doctor/export surfaces can report leftovers
    /// so a typo'd id doesn't silently patch nothing.
    pub fn dangling_model_patches(&self) -> Vec<&str> {
        self.patch
            .models
            .keys()
            .filter(|id| !self.models.contains_key(*id))
            .map(String::as_str)
            .collect()
    }

    pub fn merge_from(&mut self, overlay: &ProvidersConfig) {
        for (name, provider) in &overlay.providers {
            match self.providers.get_mut(name) {
                Some(existing) => existing.merge_from(provider),
                None => {
                    self.providers.insert(name.clone(), provider.clone());
                }
            }
        }
        self.aliases.extend(overlay.aliases.clone());
        self.alias_tool_calling
            .extend(overlay.alias_tool_calling.clone());
        self.models.extend(overlay.models.clone());
        self.qc_defaults.extend(overlay.qc_defaults.clone());

        // `[patch.models]` field-wise patches. Two deliberate ordering rules
        // (see [`PatchDef`]):
        //   1. Within one overlay, the whole-row `models` replacement above
        //      lands first, then patches — so `[patch.models.X]` fields win
        //      over the same overlay's `[models.X]` row.
        //   2. Patches are sticky: the accumulator re-applies after EVERY
        //      layer's merge, so a later layer's whole-row replacement still
        //      gets earlier layers' field tweaks re-applied on top. A patch
        //      means "always tweak this field", not "tweak it once".
        // Per-id patches from later layers deep-merge into the accumulator
        // (later layer wins per field), so two layers patching different
        // fields of the same row both stay sticky.
        // Short-circuit when no layer has contributed a patch so existing
        // patch-free configs pay nothing here.
        if !overlay.patch.models.is_empty() || !self.patch.models.is_empty() {
            for (id, patch) in &overlay.patch.models {
                match self.patch.models.get_mut(id) {
                    Some(existing) => deep_merge_toml(existing, patch),
                    None => {
                        self.patch.models.insert(id.clone(), patch.clone());
                    }
                }
            }
            apply_model_patches(&mut self.models, &self.patch.models);
        }

        if overlay.default_provider.is_some() {
            self.default_provider = overlay.default_provider.clone();
        }
        // `fallback_model` is the retired spelling of "the default provider's
        // default model". Normalize it here, at the one merge every overlay
        // passes through, into that provider's `runtime` default, so older
        // overlays keep working and no resolver sees a second mechanism. An
        // explicit `provider_defaults` entry below still wins.
        if let Some(model) = &overlay.fallback_model {
            if let Some(provider) = overlay
                .default_provider
                .clone()
                .or_else(|| self.default_provider.clone())
            {
                self.provider_defaults.entry(provider).or_default().runtime = Some(model.clone());
            }
        }
        for (provider, defaults) in &overlay.provider_defaults {
            let current = self.provider_defaults.entry(provider.clone()).or_default();
            if defaults.runtime.is_some() {
                current.runtime = defaults.runtime.clone();
            }
            if defaults.portal.is_some() {
                current.portal = defaults.portal.clone();
            }
        }

        if overlay.data_controls_audit.is_some() {
            self.data_controls_audit = overlay.data_controls_audit.clone();
        }
        if overlay.data_controls_policy != DataControlsPolicy::default() {
            self.data_controls_policy = overlay.data_controls_policy;
        }
        if overlay.usage_accounting_audit.is_some() {
            self.usage_accounting_audit = overlay.usage_accounting_audit.clone();
        }

        if !overlay.inference_rules.is_empty() {
            let mut merged = overlay.inference_rules.clone();
            merged.extend(self.inference_rules.clone());
            self.inference_rules = merged;
        }

        if !overlay.tier_rules.is_empty() {
            let mut merged = overlay.tier_rules.clone();
            merged.extend(self.tier_rules.clone());
            self.tier_rules = merged;
        }

        if overlay.tier_defaults.default != default_mid() {
            self.tier_defaults = overlay.tier_defaults.clone();
        }

        for (pattern, defaults) in &overlay.model_defaults {
            self.model_defaults
                .entry(pattern.clone())
                .or_default()
                .extend(defaults.clone());
        }

        for (role, defaults) in &overlay.model_roles {
            self.model_roles
                .entry(role.clone())
                .or_default()
                .extend(defaults.clone());
        }

        for route in &overlay.suppress.routes {
            if !self.suppress.routes.contains(route) {
                self.suppress.routes.push(route.clone());
            }
        }

        // Named model ladders: later layers replace a same-named ladder
        // wholesale (a ladder is an ordered list; per-step merge has no
        // sensible meaning), matching `[models]` whole-row replace semantics.
        self.model_ladders.extend(overlay.model_ladders.clone());

        // Presentation rows are stable-id records. A later layer owns and
        // replaces the complete same-id row, just like `[models]` and named
        // model ladders; ordered dimensions/presets have no useful field-wise
        // merge semantics.
        self.presentation
            .variants
            .extend(overlay.presentation.variants.clone());
        self.presentation
            .families
            .extend(overlay.presentation.families.clone());
        // The featured list is one ordered array, so a later layer that names
        // it owns it outright — same whole-array replace rule the merge uses
        // everywhere else.
        if !overlay.presentation.featured_providers.is_empty() {
            self.presentation
                .featured_providers
                .clone_from(&overlay.presentation.featured_providers);
        }
    }
}

/// Recursively merge `overlay` into `base`. Tables merge key-by-key; every
/// other value shape — scalars AND arrays — replaces the base value
/// wholesale. Replacing arrays instead of merging them is the documented
/// convention: there is no sane universal element-wise merge for lists like
/// `capabilities` or `strengths`, so a patch that names an array owns it.
fn deep_merge_toml(base: &mut toml::Value, overlay: &toml::Value) {
    match (base, overlay) {
        (toml::Value::Table(base_table), toml::Value::Table(overlay_table)) => {
            for (key, overlay_value) in overlay_table {
                match base_table.get_mut(key) {
                    Some(base_value) => deep_merge_toml(base_value, overlay_value),
                    None => {
                        base_table.insert(key.clone(), overlay_value.clone());
                    }
                }
            }
        }
        (base_slot, overlay_value) => *base_slot = overlay_value.clone(),
    }
}

/// True once a type-invalid `[patch.models]` entry has been reported.
/// Patches re-apply on every layer merge (stickiness), so an unconditional
/// eprintln would repeat the same diagnostic once per layer per process.
static MODEL_PATCH_TYPE_ERROR_WARNED: AtomicBool = AtomicBool::new(false);

/// Apply every accumulated `[patch.models]` entry to its matching model row.
///
/// Patch application is `ModelDef -> toml::Value -> deep merge -> ModelDef`,
/// so a patch can only express states the row schema can represent. Ids with
/// no matching row are skipped (see
/// [`ProvidersConfig::dangling_model_patches`]). A patch that produces a
/// type-invalid row warns once (matching the `read_external_config` eprintln
/// precedent) and keeps the unpatched row, so one bad overlay field can't
/// take out the whole catalog entry.
fn apply_model_patches(
    models: &mut BTreeMap<String, ModelDef>,
    patches: &BTreeMap<String, toml::Value>,
) {
    for (id, patch) in patches {
        let Some(base) = models.get(id) else {
            continue;
        };
        match patched_model_row(base, patch) {
            Ok(patched) => {
                models.insert(id.clone(), patched);
            }
            Err(error) => {
                if !MODEL_PATCH_TYPE_ERROR_WARNED.swap(true, Ordering::Relaxed) {
                    eprintln!(
                        "[llm_config] invalid [patch.models.\"{id}\"] overlay \
                         (keeping the unpatched row): {error}"
                    );
                }
            }
        }
    }
}

/// Produce the patched version of one model row, or a description of why the
/// patch does not typecheck against the row schema.
fn patched_model_row(base: &ModelDef, patch: &toml::Value) -> Result<ModelDef, String> {
    let mut value = toml::Value::try_from(base)
        .map_err(|error| format!("serialize base row for patching: {error}"))?;
    deep_merge_toml(&mut value, patch);
    ModelDef::deserialize(value).map_err(|error| error.to_string())
}

#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct InferenceRule {
    #[serde(default)]
    pub pattern: Option<String>,
    #[serde(default)]
    pub contains: Option<String>,
    #[serde(default)]
    pub exact: Option<String>,
    pub provider: String,
}

#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct TierRule {
    #[serde(default)]
    pub pattern: Option<String>,
    #[serde(default)]
    pub contains: Option<String>,
    #[serde(default)]
    pub exact: Option<String>,
    pub tier: String,
}

#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct TierDefaults {
    #[serde(default = "default_mid")]
    pub default: String,
}

impl Default for TierDefaults {
    fn default() -> Self {
        Self {
            default: default_mid(),
        }
    }
}

fn default_mid() -> String {
    "mid".to_string()
}