harn-cli 0.10.29

CLI for the Harn programming language — run, test, REPL, format, and lint
Documentation
use super::*;

pub(super) fn inspect_report(args: &ModelsLoraInspectArgs) -> Result<LoraInspectReport, String> {
    if args.require_contract_id && args.manifest.is_none() {
        return Err("--require-contract-id requires --manifest".to_string());
    }
    let resolved = harn_vm::llm_config::resolve_model_info(&args.base_model);
    let provider = resolve_lora_provider(args.provider.as_deref(), &resolved.provider);
    let catalog = harn_vm::llm_config::model_catalog_entry(&resolved.id);
    let capabilities = harn_vm::llm::capabilities::lookup(&provider, &resolved.id);
    let tool_format = harn_vm::llm_config::default_tool_format(&resolved.id, &provider);
    let adapter = inspect_adapter(&args.adapter, args.name.as_deref())?;
    let local_runtime =
        harn_vm::llm_config::provider_config(&provider).and_then(|provider| provider.local_runtime);
    let provider_lora_module_value_format = lora_modules_value_format(local_runtime.as_ref());
    let provider_supports_lora_launch = local_runtime
        .as_ref()
        .and_then(|runtime| runtime.lora_modules_arg.as_ref())
        .is_some();
    let provider_supports_lora_max_rank = local_runtime
        .as_ref()
        .and_then(|runtime| runtime.max_lora_rank_arg.as_ref())
        .is_some();
    let base_model_match =
        base_model_match(adapter.base_model_name_or_path.as_deref(), &resolved.id);
    let mut warnings = Vec::new();
    if adapter.exists && !adapter.config_found {
        warnings.push("local adapter exists but adapter_config.json was not found".to_string());
    }
    if adapter.exists && adapter.weights_found.is_empty() {
        warnings.push("local adapter has no adapter_model.* weight file".to_string());
    }
    if adapter
        .peft_type
        .as_deref()
        .is_some_and(|peft| peft != "LORA")
    {
        warnings.push("adapter_config.json peft_type is not LORA".to_string());
    }
    if matches!(base_model_match, BaseModelMatch::Mismatch) {
        warnings.push(format!(
            "adapter base_model_name_or_path does not match resolved base model {}",
            resolved.id
        ));
    }
    if !adapter.exists {
        warnings.push(
            "adapter path does not exist locally; treating it as a remote/runtime-resolved id"
                .to_string(),
        );
    }
    if !provider_supports_lora_launch {
        warnings.push(format!(
            "provider {provider} does not declare local-runtime LoRA launch flags"
        ));
    }
    if adapter.rank.is_some() && provider_supports_lora_launch && !provider_supports_lora_max_rank {
        warnings.push(format!(
            "adapter rank is known but provider {provider} does not declare a max LoRA rank flag"
        ));
    }
    let contract = inspect_contract_report(
        args.manifest.as_deref(),
        args.require_contract_id,
        &adapter,
        &resolved.id,
        &provider,
        &tool_format,
    )?;
    if let Some(contract) = &contract {
        warnings.extend(contract.warnings.clone());
    }
    let ok = warnings.iter().all(|warning| {
        !warning.starts_with("local adapter exists")
            && !warning.starts_with("adapter_config.json peft_type")
            && !warning.starts_with("adapter base_model_name_or_path")
            && !warning.starts_with("LoRA contract mismatch")
            && !warning.starts_with("LoRA contract missing")
    });
    let request_model = adapter.name.clone();
    let max_lora_rank = adapter
        .rank
        .filter(|_| provider_supports_lora_launch && provider_supports_lora_max_rank);
    let harn_local_launch = if provider_supports_lora_launch {
        let model_source = adapter
            .base_model_name_or_path
            .clone()
            .unwrap_or_else(|| resolved.id.clone());
        let mut command = vec![
            "harn".to_string(),
            "local".to_string(),
            "launch".to_string(),
            args.base_model.clone(),
            "--provider".to_string(),
            provider.clone(),
            "--model-source".to_string(),
            model_source,
            "--lora-adapter".to_string(),
            format!("{}={}", adapter.name, adapter.input),
        ];
        if let Some(rank) = max_lora_rank {
            command.extend(["--max-lora-rank".to_string(), rank.to_string()]);
        }
        command
    } else {
        Vec::new()
    };
    let serving = InspectServingReport {
        request_model: request_model.clone(),
        base_model: resolved.id.clone(),
        provider: provider.clone(),
        tool_format: tool_format.clone(),
        lora_module_value_format: provider_lora_module_value_format.clone(),
        serving_requirements: tool_call_serving_requirements(&resolved.id, &provider, &tool_format),
    };
    Ok(LoraInspectReport {
        ok,
        base: BaseModelReport {
            selector: args.base_model.clone(),
            id: resolved.id.clone(),
            provider,
            resolved_alias: resolved.alias,
            tool_format,
            tier: resolved.tier,
            family: resolved.family,
            lineage: resolved.lineage,
            catalog_name: catalog.as_ref().map(|model| model.name.clone()),
            context_window: catalog.as_ref().map(|model| model.context_window),
        },
        adapter,
        contract,
        compatibility: CompatibilityReport {
            base_model_match,
            provider_supports_lora_launch,
            provider_supports_lora_max_rank,
            provider_lora_module_value_format,
        },
        tool_calling: ToolCallingReport {
            native_tools: capabilities.native_tools,
            preferred_tool_format: capabilities.preferred_tool_format,
            text_tool_wire_format_supported: capabilities.text_tool_wire_format_supported,
            structured_output_mode: capabilities.structured_output_mode,
            recommended_endpoint: capabilities.recommended_endpoint,
        },
        serving,
        launch: LaunchHints {
            request_model,
            max_lora_rank,
            harn_local_launch,
        },
        warnings,
    })
}

pub(super) fn inspect_contract_report(
    manifest_path: Option<&Path>,
    require_adapter_contract_id: bool,
    adapter: &AdapterReport,
    resolved_base_model: &str,
    provider: &str,
    tool_format: &str,
) -> Result<Option<InspectContractReport>, String> {
    let Some(path) = manifest_path else {
        return Ok(None);
    };
    let raw = std::fs::read_to_string(path)
        .map_err(|error| format!("failed to read manifest {}: {error}", path.display()))?;
    let manifest = serde_json::from_str::<serde_json::Value>(&raw)
        .map_err(|error| format!("failed to parse manifest {}: {error}", path.display()))?;
    let contract = manifest
        .get("contract")
        .and_then(serde_json::Value::as_object)
        .ok_or_else(|| format!("manifest {} is missing contract object", path.display()))?;
    let contract_id = manifest_string_from_object(contract, "id");
    let manifest_base_model = manifest_string_from_object(contract, "base_model");
    let manifest_provider = manifest_string_from_object(contract, "provider");
    let manifest_tool_format = manifest_string_from_object(contract, "harn_tool_format");
    let manifest_dataset_format = manifest_string_from_object(contract, "dataset_format");
    let manifest_chat_template = manifest_string_from_object(contract, "chat_template");
    let manifest_target_modules = manifest_target_modules_from_contract(contract);
    let manifest_modules_to_save = manifest_modules_to_save_from_contract(contract);
    let target_adapter_name = manifest
        .get("target")
        .and_then(serde_json::Value::as_object)
        .and_then(|target| manifest_string_from_object(target, "adapter_name"));
    let serving_request_model = manifest
        .get("serving")
        .and_then(serde_json::Value::as_object)
        .and_then(|serving| manifest_string_from_object(serving, "request_model"));

    let base_model_match = base_model_match(manifest_base_model.as_deref(), resolved_base_model);
    let provider_matches = manifest_provider
        .as_deref()
        .is_some_and(|manifest_provider| manifest_provider == provider);
    let tool_format_matches = manifest_tool_format
        .as_deref()
        .is_some_and(|manifest_tool_format| manifest_tool_format == tool_format);
    let adapter_name_expectations = [
        target_adapter_name.as_deref(),
        serving_request_model.as_deref(),
    ];
    let adapter_name_matches = if adapter_name_expectations.iter().any(Option::is_some) {
        Some(
            adapter_name_expectations
                .into_iter()
                .flatten()
                .all(|expected| expected == adapter.name),
        )
    } else {
        None
    };
    let adapter_contract_id_matches = match (&adapter.contract_id, &contract_id) {
        (Some(adapter_id), Some(manifest_id)) => Some(adapter_id == manifest_id),
        _ => None,
    };
    let adapter_modules_to_save = normalize_modules_to_save_lossy(adapter.modules_to_save.clone());
    let adapter_target_modules = normalize_target_modules(&adapter.target_modules)
        .unwrap_or_else(|_| adapter.target_modules.clone());
    let target_modules_match = manifest_target_modules
        .as_ref()
        .map(|manifest_target| manifest_target.modules == adapter_target_modules);
    let modules_to_save_matches = manifest_modules_to_save
        .as_ref()
        .map(|manifest_modules| manifest_modules == &adapter_modules_to_save);

    let mut warnings = Vec::new();
    if matches!(
        base_model_match,
        BaseModelMatch::Mismatch | BaseModelMatch::Unknown
    ) {
        warnings.push(format!(
            "LoRA contract mismatch: manifest base_model={} does not match resolved base {}",
            manifest_base_model.as_deref().unwrap_or("<missing>"),
            resolved_base_model
        ));
    }
    if !provider_matches {
        warnings.push(format!(
            "LoRA contract mismatch: manifest provider={} does not match provider {}",
            manifest_provider.as_deref().unwrap_or("<missing>"),
            provider
        ));
    }
    if !tool_format_matches {
        warnings.push(format!(
            "LoRA contract mismatch: manifest tool format={} does not match route tool format {}",
            manifest_tool_format.as_deref().unwrap_or("<missing>"),
            tool_format
        ));
    }
    if contract_id.is_none() {
        warnings.push("LoRA contract mismatch: manifest contract.id is missing".to_string());
    }
    if adapter_name_matches == Some(false) {
        warnings.push(format!(
            "LoRA contract mismatch: manifest adapter/request model does not match adapter name {}",
            adapter.name
        ));
    }
    if adapter_contract_id_matches == Some(false) {
        warnings.push(format!(
            "LoRA contract mismatch: adapter contract id {} does not match manifest contract id {}",
            adapter.contract_id.as_deref().unwrap_or("<missing>"),
            contract_id.as_deref().unwrap_or("<missing>")
        ));
    }
    if manifest_modules_to_save.is_none() {
        warnings.push("LoRA contract mismatch: manifest PEFT save policy is missing".to_string());
    } else if modules_to_save_matches == Some(false) {
        warnings.push(format!(
            "LoRA contract mismatch: adapter modules_to_save {:?} does not match manifest modules_to_save {:?}",
            adapter_modules_to_save,
            manifest_modules_to_save.as_deref().unwrap_or(&[])
        ));
    }
    if manifest_target_modules.is_none() {
        warnings
            .push("LoRA contract mismatch: manifest target-module contract is missing".to_string());
    } else if target_modules_match == Some(false) {
        warnings.push(format!(
            "LoRA contract mismatch: adapter target_modules {:?} does not match manifest target_modules {:?}",
            adapter_target_modules,
            manifest_target_modules
                .as_ref()
                .map(|contract| contract.modules.as_slice())
                .unwrap_or(&[])
        ));
    }
    if adapter.contract_id.is_none() {
        let prefix = if require_adapter_contract_id {
            "LoRA contract missing"
        } else {
            "LoRA contract warning"
        };
        warnings.push(format!(
            "{prefix}: adapter_config.json does not include harn_lora_contract_id"
        ));
    }

    let status = if warnings.iter().any(|warning| {
        warning.starts_with("LoRA contract mismatch")
            || warning.starts_with("LoRA contract missing")
    }) {
        ContractCheckStatus::Fail
    } else if warnings.is_empty() {
        ContractCheckStatus::Pass
    } else {
        ContractCheckStatus::Warn
    };

    Ok(Some(InspectContractReport {
        manifest_path: path.display().to_string(),
        contract_id,
        adapter_contract_id: adapter.contract_id.clone(),
        status,
        base_model_match,
        provider_matches,
        tool_format_matches,
        adapter_name_matches,
        target_modules_match,
        modules_to_save_matches,
        require_adapter_contract_id,
        manifest: InspectContractManifest {
            base_model: manifest_base_model,
            provider: manifest_provider,
            harn_tool_format: manifest_tool_format,
            dataset_format: manifest_dataset_format,
            chat_template: manifest_chat_template,
            target_modules: manifest_target_modules,
            modules_to_save: manifest_modules_to_save,
            adapter_name: target_adapter_name,
            request_model: serving_request_model,
        },
        warnings,
    }))
}

pub(super) fn manifest_target_modules_from_contract(
    contract: &serde_json::Map<String, serde_json::Value>,
) -> Option<TargetModuleContract> {
    let target = contract.get("target_modules")?.as_object()?;
    let policy = manifest_string_from_object(target, "policy")?;
    let modules = normalize_target_modules(&value_string_list(target.get("modules")?)).ok()?;
    Some(TargetModuleContract { policy, modules })
}

pub(super) fn manifest_modules_to_save_from_contract(
    contract: &serde_json::Map<String, serde_json::Value>,
) -> Option<Vec<String>> {
    let modules = contract
        .get("training_contract")?
        .get("peft_save_policy")?
        .get("modules_to_save")?;
    Some(normalize_modules_to_save_lossy(value_string_list(modules)))
}

pub(super) fn manifest_string_from_object(
    object: &serde_json::Map<String, serde_json::Value>,
    key: &str,
) -> Option<String> {
    object
        .get(key)
        .and_then(serde_json::Value::as_str)
        .filter(|value| !value.is_empty())
        .map(str::to_string)
}