{"id":50331783,"url":"https://github.com/rupurt/metamorph","last_synced_at":"2026-05-29T10:01:46.812Z","repository":{"id":348639202,"uuid":"1199102511","full_name":"rupurt/metamorph","owner":"rupurt","description":"Model format conversion for local-first AI runtimes.","archived":false,"fork":false,"pushed_at":"2026-04-02T17:23:36.000Z","size":71,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-02T17:27:11.227Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rupurt.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-02T03:44:15.000Z","updated_at":"2026-04-02T17:23:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rupurt/metamorph","commit_stats":null,"previous_names":["rupurt/metamorph"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/rupurt/metamorph","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rupurt%2Fmetamorph","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rupurt%2Fmetamorph/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rupurt%2Fmetamorph/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rupurt%2Fmetamorph/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rupurt","download_url":"https://codeload.github.com/rupurt/metamorph/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rupurt%2Fmetamorph/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33646428,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-29T10:01:42.547Z","updated_at":"2026-05-29T10:01:46.805Z","avatar_url":"https://github.com/rupurt.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# metamorph\n\n`metamorph` is a Rust library and CLI for turning model artifacts from the format they are published in into the format a downstream runtime can actually load.\n\nIt is aimed at two audiences:\n\n- CLI operators who need a repeatable inspect -\u003e plan -\u003e convert -\u003e validate -\u003e publish workflow\n- Rust integrators who want to embed that workflow instead of rebuilding it in one-off scripts\n\n## What Metamorph Does Today\n\nMetamorph currently ships a real, end-to-end local conversion path and the supporting planning surfaces around it.\n\n| Surface | Works today | Important notes |\n| --- | --- | --- |\n| `inspect` | Local paths and `hf://repo[@revision]` sources | Hugging Face inspection is heuristic today; it infers format from the repo name rather than remote file listing |\n| `compatibility` / `plan` | Local paths and `hf://...` sources | Use `--from` or set `ConvertRequest { from: Some(...), .. }` when the source format cannot be inferred |\n| `convert` execution | `gguf -\u003e hf-safetensors`, `gguf -\u003e safetensors`, `safetensors -\u003e safetensors`, `hf-safetensors -\u003e hf-safetensors`, `safetensors -\u003e hf-safetensors` | GGUF conversion supports representative remote `hf://...` GGUF fetch; the relayout and bundle-materialization paths are local-only and metadata-gated where required |\n| `validate` | Local `safetensors` files and local `hf-safetensors` bundles | Passing outputs are marked reusable |\n| `cache` | Deterministic cache identity, local materialization, remote fetch/reuse/refresh reporting | The current remote slice supports representative GGUF repos that expose exactly one GGUF artifact per revision |\n| `upload` | Preview and execute publish for local `hf-safetensors` bundles | `--execute` requires `HF_TOKEN`, targets an existing Hugging Face repo on `main`, and reports `complete`, `partial`, `guarded-refusal`, or `failed` outcomes explicitly |\n\nExecutable conversion backends today:\n\n- `gguf -\u003e hf-safetensors`\n- `gguf -\u003e safetensors`\n- `safetensors -\u003e safetensors`\n- `hf-safetensors -\u003e hf-safetensors`\n- `safetensors -\u003e hf-safetensors`\n\nThere is no blanket same-format placeholder anymore. Requests such as `gguf -\u003e gguf` that do not yet have a truthful reusable-output contract are reclassified as `unsupported` instead of being labeled as vaguely planned.\n\n## What Metamorph Does Not Do Yet\n\nThese gaps matter for both CLI usage and embedding:\n\n- It does not treat every format pair or same-format request as executable just because the names look compatible.\n- It does not fetch every Hugging Face repository layout; the current remote slice is limited to representative GGUF repos with one GGUF artifact per revision.\n- It does not create repos, choose alternate publish branches, or support non-Hugging-Face publish targets yet.\n- It does not hide lossy conversion behind a silent fallback.\n\nIf a path is unknown, unsupported, or blocked by missing lossy opt-in, missing metadata, or local-only execution limits, Metamorph is expected to say so explicitly.\n\n## Quick Start\n\nEnter the dev environment:\n\n```bash\ndirenv allow\nnix develop\n```\n\nInside the shell, the CLI is available as `metamorph`.\n\nTop-level help:\n\n```bash\nmetamorph --help\n```\n\n## CLI Workflows\n\n### 1. Inspect a source\n\nInspect tells you what Metamorph thinks the source is and why.\n\n```bash\nmetamorph inspect hf://prism-ml/Bonsai-8B-gguf@main\n```\n\nExample output:\n\n```text\nSource: hf://prism-ml/Bonsai-8B-gguf@main\nDetected format: gguf\nNotes:\n- using pinned revision `main`\n```\n\nUse `inspect` first when you are not sure whether a path is a single GGUF file, a plain safetensors artifact, or a Hugging Face-style bundle.\n\n### 2. Plan a conversion before executing it\n\n`convert --plan-only` is the highest-signal CLI entry point. It shows:\n\n- compatibility status\n- resolved source format\n- target format\n- whether the path is lossy\n- which backend would run\n- blockers\n- planned conversion steps\n\n```bash\nmetamorph convert \\\n  --input hf://prism-ml/Bonsai-8B-gguf@main \\\n  --output ./tmp/bonsai-candle \\\n  --to hf-safetensors \\\n  --allow-lossy \\\n  --plan-only\n```\n\nExample output:\n\n```text\nCompatibility status: executable\nResolved source format: gguf\nRequested target format: hf-safetensors\nLossy: true\nCompatible backend: gguf-to-hf-safetensors\nCompatibility notes:\n- using pinned revision `main`\nPlanned conversion: gguf -\u003e hf-safetensors\nTarget: ./tmp/bonsai-candle\nExecution: executable\nBackend: gguf-to-hf-safetensors\nLossy: true\nSteps:\n- fetch or read GGUF artifacts\n- materialize tensors into a Hugging Face-style safetensors layout\n- emit tokenizer and config files expected by downstream runtimes\n- validate the output bundle\nNotes:\n- using pinned revision `main`\n```\n\nImportant distinction:\n\n- compatibility and planning work on local sources and `hf://...` sources\n- execution can fetch a representative remote GGUF source on demand into the managed cache\n- use `--from gguf` when the remote repo name is too ambiguous to infer\n- use `--refresh` when you want to bypass reusable remote cache state explicitly\n\n### 3. Execute a conversion\n\nFor local execution, point `--input` at a local source and use a local output path.\n\n```bash\nmetamorph convert \\\n  --input ./models/bonsai.gguf \\\n  --from gguf \\\n  --to hf-safetensors \\\n  --output ./artifacts/bonsai-candle \\\n  --allow-lossy\n```\n\nFor plain safetensors output:\n\n```bash\nmetamorph convert \\\n  --input ./models/bonsai.gguf \\\n  --from gguf \\\n  --to safetensors \\\n  --output ./artifacts/bonsai.safetensors \\\n  --allow-lossy\n```\n\nIf the safetensors output path is a directory rather than a `.safetensors` file, Metamorph writes `model.safetensors` inside that directory.\n\nFor local relayout of existing safetensors artifacts:\n\n```bash\nmetamorph convert \\\n  --input ./artifacts/original \\\n  --output ./artifacts/normalized \\\n  --to safetensors\n```\n\nFor local promotion of a plain safetensors source into an `hf-safetensors` bundle, the source must provide:\n\n- exactly one `.safetensors` artifact\n- `config.json`\n- `tokenizer.json`\n\nIf `generation_config.json` is missing, Metamorph writes an empty generation config into the target bundle and reports that note in planning output.\n\nCurrent execution rules:\n\n- `--allow-lossy` is required for both executable GGUF conversion paths\n- conversion outputs must be local filesystem targets\n- representative remote `hf://...` GGUF sources are fetched on demand before backend execution\n- `safetensors -\u003e safetensors`, `hf-safetensors -\u003e hf-safetensors`, and `safetensors -\u003e hf-safetensors` are local-only execution paths\n- `safetensors -\u003e hf-safetensors` currently expects one local safetensors artifact plus `config.json` and `tokenizer.json`\n- `--refresh` forces remote re-fetch instead of reusing an existing managed cache artifact\n- the current remote slice expects exactly one GGUF artifact at the selected repo revision\n- `hf://repo` is a publish destination, not a direct conversion target\n\n### 4. Validate an output bundle\n\nValidation tells you whether a local artifact satisfies a reusable contract.\n\n```bash\nmetamorph validate ./artifacts/bonsai-candle --format hf-safetensors\n```\n\nUse `--format` when you want to enforce a specific downstream contract rather than just infer it.\n\nSupported validation contracts today:\n\n- `hf-safetensors`\n- `safetensors`\n\n### 5. Inspect cache state and materialize local sources\n\nShow the managed cache root:\n\n```bash\nmetamorph cache dir\n```\n\nInspect the deterministic cache identity for a source:\n\n```bash\nmetamorph cache source hf://prism-ml/Bonsai-8B-gguf@main\n```\n\nMaterialize a managed copy of a local source:\n\n```bash\nmetamorph cache source ./models/bonsai.gguf --from gguf --materialize\n```\n\nForce a remote refresh instead of reuse:\n\n```bash\nmetamorph cache source hf://prism-ml/Bonsai-8B-gguf@main --refresh\n```\n\n`cache source` reports:\n\n- cache key\n- cache path\n- source format\n- status such as `reused-local-path`, `materialized-local-copy`, `reused-managed-local-copy`, `reused-remote-cache`, `fetched-remote`, or `refreshed-remote`\n- the resolved path Metamorph would use next\n\n### 6. Preview a publish without mutating anything\n\n`upload` is preview-first.\n\n```bash\nmetamorph upload \\\n  --input ./artifacts/bonsai-candle \\\n  --repo your-org/Bonsai-8B-candle\n```\n\nThis validates the local bundle, lists the artifacts that would be published, and explains the next step. It does not perform a remote write.\n\n`--execute` is explicit:\n\n```bash\nmetamorph upload \\\n  --input ./artifacts/bonsai-candle \\\n  --repo your-org/Bonsai-8B-candle \\\n  --execute\n```\n\nCurrent behavior with `--execute`:\n\n- requires `HF_TOKEN`\n- targets an explicitly named existing Hugging Face repo on `main`\n- keeps preview mode and execute mode distinct\n- reports `complete`, `partial`, `guarded-refusal`, or `failed`\n- surfaces per-artifact status such as `pending`, `transferred`, `published`, `already-present`, or `failed`\n- prints retry guidance when a partial publish leaves remaining work\n\n## CLI Command Reference\n\nTop-level commands:\n\n- `metamorph inspect \u003cINPUT\u003e`\n- `metamorph convert --input \u003cINPUT\u003e --output \u003cOUTPUT\u003e --to \u003cFORMAT\u003e [--from \u003cFORMAT\u003e] [--allow-lossy] [--plan-only] [--refresh]`\n- `metamorph validate \u003cPATH\u003e [--format \u003cFORMAT\u003e]`\n- `metamorph upload --input \u003cPATH\u003e --repo \u003cOWNER/NAME\u003e [--execute]`\n- `metamorph cache dir`\n- `metamorph cache source \u003cINPUT\u003e [--from \u003cFORMAT\u003e] [--materialize] [--refresh]`\n\nAccepted source forms:\n\n- local file path\n- local directory path\n- `hf://owner/repo`\n- `hf://owner/repo@revision`\n\nFormats understood today:\n\n- `gguf`\n- `safetensors`\n- `hf-safetensors`\n- `mlx`\n\n## Library Integration Guide\n\nThe library is the source of truth. The CLI is intentionally thin and mostly renders the reports returned by the library.\n\n### Public workflow types\n\nThe main public workflow is built around these types:\n\n- `Source`\n- `Target`\n- `Format`\n- `ConvertRequest`\n- `CompatibilityReport`\n- `ConversionPlan`\n- `ValidationReport`\n- `CacheIdentity`\n- `SourceAcquisitionReport`\n- `PublishPlan`\n- `PublishStatus`\n- `PublishArtifactStatus`\n- `PublishArtifactReport`\n- `PublishReport`\n\n### Plan before executing\n\nFor integrations, the safest default is:\n\n1. parse a `Source`\n2. call `compatibility()`\n3. inspect `status`, `backend`, and `blockers`\n4. only call `convert()` when the path is executable and unblocked\n\n```rust\nuse std::path::Path;\nuse std::str::FromStr;\n\nuse metamorph::{\n    compatibility, convert, validate, CompatibilityStatus, ConvertRequest, Format, Source, Target,\n};\n\nfn convert_local_model() -\u003e metamorph::Result\u003c()\u003e {\n    let request = ConvertRequest {\n        source: Source::from_str(\"./models/bonsai.gguf\")?,\n        target: Target::LocalDir(\"./artifacts/bonsai-candle\".into()),\n        from: Some(Format::Gguf),\n        to: Format::HfSafetensors,\n        allow_lossy: true,\n        refresh_remote: false,\n    };\n\n    let report = compatibility(\u0026request)?;\n    if report.status != CompatibilityStatus::Executable || !report.blockers.is_empty() {\n        eprintln!(\"conversion is not ready: {report:#?}\");\n        return Ok(());\n    }\n\n    convert(\u0026request)?;\n    let validation = validate(\n        Path::new(\"./artifacts/bonsai-candle\"),\n        Some(Format::HfSafetensors),\n    )?;\n    assert!(validation.reusable);\n\n    Ok(())\n}\n```\n\nWhy check both `status` and `blockers`:\n\n- `status` tells you whether a compatible backend class exists\n- `blockers` tells you whether the specific request is still gated by things like missing lossy opt-in, local-only execution, or missing metadata sidecars\n\n### Treat transport and conversion as separate concerns\n\n`compatibility()` and `plan()` can reason about remote `hf://...` sources without downloading them.\n\n`convert()` still resolves the input through source acquisition. That means:\n\n- local sources execute directly\n- representative remote GGUF sources fetch on demand into deterministic managed cache paths\n- `ConvertRequest { refresh_remote: true, .. }` forces a re-fetch instead of remote cache reuse\n- broader remote repo layouts are still intentionally bounded and return explicit recovery guidance\n\nRelevant helpers:\n\n- `inspect()` for source detection\n- `cache_identity()` for deterministic cache location\n- `acquire_source()` for default reuse or fetch behavior\n- `acquire_source_with_options()` when you need explicit refresh control\n\n### Use validation as the reusable-output gate\n\nIf you intend to cache, reuse, or publish an output, validate it first.\n\n```rust\nuse std::path::Path;\n\nuse metamorph::{validate, Format};\n\nfn validate_bundle() -\u003e metamorph::Result\u003c()\u003e {\n    let report = validate(Path::new(\"./artifacts/bonsai-candle\"), Some(Format::HfSafetensors))?;\n    assert!(report.reusable);\n    Ok(())\n}\n```\n\n### Use publish status as the execution gate\n\n`plan_publish()` is still useful for preview and repo-name validation, but executable integrations should branch on `PublishStatus`.\n\n```rust\nuse std::path::PathBuf;\n\nuse metamorph::{publish, PublishRequest, PublishStatus, Target};\n\nfn publish_bundle() -\u003e metamorph::Result\u003c()\u003e {\n    let report = publish(\u0026PublishRequest {\n        input: PathBuf::from(\"./artifacts/bonsai-candle\"),\n        target: Target::HuggingFaceRepo(\"your-org/Bonsai-8B-candle\".into()),\n        execute: true,\n    })?;\n\n    match report.status {\n        PublishStatus::Complete =\u003e assert!(report.executed),\n        PublishStatus::Preview =\u003e unreachable!(\"execute=true should not yield preview\"),\n        PublishStatus::GuardedRefusal | PublishStatus::Partial | PublishStatus::Failed =\u003e {\n            eprintln!(\"publish needs attention: {report:#?}\");\n        }\n    }\n\n    Ok(())\n}\n```\n\nUse the per-artifact reports to distinguish:\n\n- files already present upstream\n- artifacts transferred before a partial failure\n- artifacts still pending retry\n- artifacts that failed outright\n\n## Behavioral Guarantees\n\nThe current repo contract is:\n\n- lossy conversions require explicit opt-in\n- compatibility reporting and execution dispatch come from the same registry-driven truth\n- validation is part of the conversion workflow, not an optional afterthought\n- caching is deterministic and inspectable\n- upload is preview-first\n- upload execution is explicit and reports guarded refusal, complete, partial, and failed states from the library\n- the library surface stays ahead of the CLI surface\n\n## Repository Map\n\n- `crates/metamorph/` is the reusable library\n- `crates/metamorph-cli/` is the thin CLI\n- [USER_GUIDE.md](USER_GUIDE.md) is the operator playbook\n- [ARCHITECTURE.md](ARCHITECTURE.md) describes system boundaries\n- [CODE_WALKTHROUGH.md](CODE_WALKTHROUGH.md) maps commands and features to source files\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frupurt%2Fmetamorph","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frupurt%2Fmetamorph","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frupurt%2Fmetamorph/lists"}