/**
* `eval/model_selector` — `.harn` port of the model-selector resolver
* helper shared across `harn eval *` commands. See harn#2306 (W6).
*
* **Helper-only module.** The Rust-side `eval_model_selector.rs` is not
* a CLI subcommand — it is a pure helper invoked by `eval_tool_calls`,
* `eval_coding_agent`, etc. Porting it here gives the `.harn`-side
* renderers a parallel implementation they can call once the wider
* cluster ports land. Today, callers are still on the Rust helper.
*
* Why the parallel impl ships now: the W6 epic explicitly asked for
* `model_selector` alongside `context` and `tool_calls` (see #2306).
* Embedding it here pins the .harn surface so the renderers in the
* same wave can reach for the resolver without round-tripping through
* the Rust shim. The alias-resolution branch (the third leg of the
* Rust resolver — `harn_vm::llm_config::resolve_model_info`) is not
* reachable from script-land today and is delegated to the shim via
* `HARN_MODEL_SELECTOR_ALIASES_JSON` (an alias→{provider, model} map
* the Rust caller pre-resolves once and forwards). G4 (#2297) will
* expose the provider catalog directly to scripts and let us drop the
* env hand-off.
*
* The script's `main` is a tiny REPL-style entrypoint that resolves a
* single selector from `HARN_MODEL_SELECTOR_INPUT` and prints the
* resolved `{selector, provider, model}` as JSON. It exists so the
* dispatch wedge has a stable invocation surface; production callers
* use the helpers below directly from sibling scripts.
*/
/**
* Parse a `provider=...,model=...` comma-separated key-value form
* (used by `--planner provider=openrouter,model=google/gemma` etc).
* Returns `{provider, model}` on success or `nil` when either key is
* absent or empty. Mirrors `parse_provider_model_kv` in the Rust
* helper.
*/
fn __parse_provider_model_kv(raw: string) -> dict {
var provider = ""
var model = ""
for part in split(raw, ",") {
let idx = part.index_of("=")
if idx < 0 {
return {}
}
let key = part[0:idx].trim()
let value = part[idx + 1:len(part)].trim()
if key == "provider" {
provider = value
} else if key == "model" {
model = value
}
}
if provider == "" || model == "" {
return {}
}
return {provider: provider, model: model}
}
/**
* Resolve a raw selector string into `{selector, provider, model}`.
*
* Resolution precedence — matches the Rust `resolve_selector`:
* 1. `provider=...,model=...` key-value form.
* 2. `provider:model` colon-delimited form.
* 3. Alias lookup via the pre-resolved `aliases` dict (the Rust
* caller supplies this via `HARN_MODEL_SELECTOR_ALIASES_JSON`).
* When the alias is absent the selector falls back to itself as
* both the provider and model — the legacy Rust path used the
* VM's `llm_config::resolve_model_info` which returns the same
* string in both fields for an unknown alias.
*/
fn resolve_selector(raw: string, aliases: dict) -> dict {
let trimmed = raw.trim()
let kv = __parse_provider_model_kv(trimmed)
if len(keys(kv)) > 0 {
return {selector: trimmed, provider: kv["provider"], model: kv["model"]}
}
let colon_idx = trimmed.index_of(":")
if colon_idx >= 0 {
let provider = trimmed[0:colon_idx]
let model = trimmed[colon_idx + 1:len(trimmed)]
if provider != "" && model != "" {
return {selector: trimmed, provider: provider, model: model}
}
}
let resolved = aliases[trimmed]
if type_of(resolved) == "dict" {
let provider = __safe_string_local(resolved["provider"], trimmed)
let model = __safe_string_local(resolved["model"], trimmed)
return {selector: trimmed, provider: provider, model: model}
}
return {selector: trimmed, provider: trimmed, model: trimmed}
}
/**
* Render a selector as `provider:model`, matching the Rust
* `selector_label`. Used in human-readable summaries.
*/
fn selector_label(selector: dict) -> string {
return __safe_string_local(selector["provider"], "")
+ ":"
+ __safe_string_local(selector["model"], "")
}
/**
* Return true when the resolved selector targets a local provider
* (matches the Rust `selector_is_local`). Used by the runner to skip
* cloud-only preflight checks on local-model phases.
*/
fn selector_is_local(selector: dict) -> bool {
let provider = __safe_string_local(selector["provider"], "")
return provider == "ollama"
|| provider == "llamacpp"
|| provider == "mlx"
|| provider == "local"
|| provider == "vllm"
|| provider == "tgi"
}
/**
* Private `__safe_string` (kept local so this module stays
* self-contained and re-readable as a unit). Kept byte-identical with
* the copy in `_runner.harn`; the parity tests catch any drift.
*/
fn __safe_string_local(value, fallback: string) -> string {
if type_of(value) == "string" {
return value
}
return fallback
}
/**
* Dispatch entrypoint for `HARN_CLI_IMPL=harn` callers that want to
* resolve a single selector through the .harn helper. The Rust shim
* has no such caller today — the helper is consumed in-process by
* sibling .harn scripts — but exposing `main` keeps the dispatch
* surface symmetrical and lets the parity tests black-box the helper
* via the wedge.
*
* Inputs:
* HARN_MODEL_SELECTOR_INPUT — raw selector string.
* HARN_MODEL_SELECTOR_ALIASES_JSON — JSON dict mapping alias →
* {provider, model}. Optional
* (defaults to `{}`).
*
* Output: one line of compact JSON on stdout —
* `{"selector":"...","provider":"...","model":"..."}`.
*/
fn main(harness: Harness) -> int {
let raw = harness.env.get_or("HARN_MODEL_SELECTOR_INPUT", "")
if raw == "" {
harness.stdio
.eprintln("internal error: HARN_MODEL_SELECTOR_INPUT not set")
return 70
}
let aliases_json = harness.env.get_or("HARN_MODEL_SELECTOR_ALIASES_JSON", "{}")
let aliases = try {
json_parse(aliases_json)
} catch (e) {
harness.stdio.eprintln("internal error: failed to parse aliases JSON: " + to_string(e))
return 70
}
let resolved = resolve_selector(raw, aliases)
// Extend the resolved record with `label` and `is_local` so a
// dispatch caller can read the full helper surface in a single
// round-trip. Also keeps the linter from flagging the helpers as
// unused — they're public API for sibling scripts but the main
// entrypoint is currently the only consumer.
let labelled = resolved + {label: selector_label(resolved), is_local: selector_is_local(resolved)}
harness.stdio.println(json_stringify(labelled))
return 0
}