Expand model package with authoring tools and schema versioning#28989
Expand model package with authoring tools and schema versioning#28989jambayk wants to merge 48 commits into
Conversation
Adds the minimal JSON utility API (OrtJson_*) the redesign needs so consumers (ORT CreateSession, GenAI, publisher tools, tests) can parse, navigate, build, mutate, and serialize JSON values without bringing their own JSON dependency. Backed by nlohmann::ordered_json so object key order is preserved across parse and round-trip. Pointer-invalidation is scoped per the design in §11.3 of model_package_redesign.md: a Set/Remove/Append on container X invalidates views into X and its descendants, but not into unrelated subtrees. Also introduces shared status plumbing used by both ModelPackage_* and OrtJson_*: - ModelPackageErrorCode enum (additive) - ModelPackage_GetErrorCode accessor - Internal status_impl.h shared between translation units - Existing ModelPackage_* error sites updated to provide a code Tests: 15 standalone test cases covering parse, build, round-trip, type errors, key-order, Unicode, file parse, uint64 overflow, view-rejection on mutation, and serialize cache stability. Built under MODEL_PACKAGE_BUILD_TESTS. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the new ModelPackage_* read API per §7.1 and §7.2 of the redesign as a
parallel implementation. Coexists with the legacy ModelPackage_CreateContext
surface for now; Phase 5 deletes the old code.
New files:
include/model_package.h - public C API (Open, inspection accessors,
round-trip JSON getters, additional_metadata)
src/model_package_impl.h - in-memory representation backed by
nlohmann::ordered_json for round-trip
src/model_package_impl.cc - C API entry points + view cache
src/manifest_parser.{h,cc} - parses manifest.json and external component
files into the in-memory model
src/path_resolver.{h,cc} - portable/installed path resolution with
confinement check; sha256: URI validator
Schema coverage (§5):
- manifest.json: schema_version (must be 1), optional package_name/version/
description/layout/additional_metadata, components map (string=external,
object=inline), shared_assets override map
- external component file or inline component (string ref to file or dir
auto-appends component.json)
- variants: optional ep/device/compatibility_string/uses_assets/
variant_directory/executor_info/additional_metadata
- executor_info entries: string (external file) or object (inline)
- uses_assets entries: sha256:<64-hex> validated
Behavioral rules:
- Portable layout rejects absolute paths and .. segments with
ERR_PATH_CONFINEMENT.
- Installed layout (manifest layout=installed or allow_external_paths
option) permits both.
- Eager check at Open: any variant with inline executor_info must have a
resolvable variant_directory; otherwise ERR_STATE (§3 principle 2).
- Strict mode (default) rejects unknown top-level fields at manifest/
component/variant scope; strict_unknown_fields=false relaxes for
round-trip of newer schemas.
- shared_assets order: declared overrides first, then any URIs referenced
in variant uses_assets but not declared (default convention path
<pkg>/shared_assets/sha256-<hex>/).
Tests: 17 standalone cases covering all the above plus round-trip preservation
of unknown fields and key order.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the directory Merkle hash from design spec §4.3.1:
* Clean-room SHA-256 implementation (src/sha256.{h,cc}) verified against
FIPS 180-4 known-answer vectors.
* Directory hash (src/asset_hasher.{h,cc}) walks the source tree, rejects
symlinks, builds a sorted manifest of '<sha256_hex> <relpath>\n' lines
using POSIX-style relative paths, then hashes the manifest text.
* New public entry point ModelPackage_ComputeDirectoryHash returns the
resulting 'sha256:<hex>' URI through a thread-local string slot.
* 13 new tests in tests/test_asset_hashing.cc covering known SHA-256
vectors, the incremental API, reproducibility, sensitivity to name
changes / content changes / swaps, empty subdir handling, symlink
rejection, and walk-order independence.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the §7.3 mutation surface in a new src/authoring.cc TU:
* ModelPackage_New: builds a minimal in-memory manifest (schema_version=1,
layout=portable, components={}) with strict_unknown_fields=true.
* ModelPackage_SetComponentInline / SetComponentExternal / RemoveComponent.
External components materialize an empty {variants:{}} body when the file
does not exist; the path becomes library-owned.
* ModelPackage_SetVariant / RemoveVariant. Upsert semantics. The §4.2 eager
inline-executor-info check fires here: a variant with object-valued
executor_info entries must have a resolvable variant_directory.
* ModelPackage_SetVariantExecutorInfoInline / SetExternal / Remove.
* ModelPackage_AddSharedAsset / RemoveSharedAsset.
copy_in=false is eagerly rejected in portable layout; copy_in=true stages
the source dir in pkg->pending_shared_asset_copies for materialization at
commit time (Phase 4). expected_uri verification supported.
* ModelPackage_SetMetadata / SetLayout / SetAdditionalMetadataJson.
Plumbing:
* manifest_parser.h gains ParseComponentBody / ParseVariantBody /
RefreshInfoView / RefreshSharedAssets / PathOptionsFor helpers so that
authoring re-uses the same validators as Open without re-implementing them.
* DropViewCache is exposed from model_package_impl.cc and invoked after every
mutation, honoring the §7.2 pointer-invalidation contract (entity handles
are rebuilt on next access).
* ModelPackage gains pending_shared_asset_copies (URI → source_dir) for the
Phase 4 commit handoff.
Tests: 24 in tests/test_authoring.cc covering each entry point, strict
field rejection, eager inline-executor-info error, upsert semantics,
metadata clear-on-empty-string, shared-asset portable rejection, expected
uri mismatch, view-cache invalidation, and round-trip via GetComponentJson.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the §7.3 / §7.4 surface in src/commit_vacuum_validate.cc:
* ModelPackage_Commit(pkg, dest_root_or_null, mode)
In-place PRESERVE: stages pending shared-asset directories under
shared_assets/sha256-<hex>.tmp.<rand>/, re-hashes after copy (TOCTOU
guard), renames into the final path, then rewrites external component
files with write-temp-then-rename, then writes manifest.json last.
Every staged file is fsync'd before its rename; containing directories
are fsync'd after. If the final asset directory already exists the
staging is discarded (the content-addressed name makes this safe).
In-place DENSE: flattens external components into the manifest before
writing; rejects external executor_info entries with ERR_STATE since
the model never loads them in memory.
dest_root ("save as"): requires empty/nonexistent target, copies all
shared assets (including overrides) into the dest_root with the
default convention, drops manifest.shared_assets overrides for a
self-contained result, enforces portable confinement on component
paths, then re-parses the freshly written package and swaps the
result into *pkg so subsequent in-place commits go to the new root.
* ModelPackage_Vacuum: walks <pkg_root>/shared_assets/, reclaiming
sha256-<hex>/ dirs not referenced by the manifest and *.tmp.* staging
dirs left behind by crashes. Both gated by a 60s grace threshold to
avoid stomping in-flight commits. Orphan component dirs are out of
scope until a convention dir is defined (spec §7.4 future work).
* ModelPackage_Validate(flags, out_report_json) with flags
SCHEMA | PATHS | ASSET_REACH | ASSET_REHASH | UNKNOWN_FIELDS | ALL.
Re-parses every component+variant body with strict=true (SCHEMA),
checks external-file existence (PATHS, warnings), enforces every
uses_assets URI resolves to a real directory (ASSET_REACH, errors),
re-hashes each shared asset and compares against its URI
(ASSET_REHASH), and warns on unknown manifest fields. Returns a JSON
report cached on pkg->last_validate_report.
Tests: 16 in tests/test_commit.cc covering both modes, dest_root save-as
+ re-parse swap, vacuum grace handling (skip recent, reclaim old
orphans and stale staging), and validate flags for clean / unknown-uri
/ missing-external / mutation-detected cases. Plus a sanity test that
no .tmp.* file remains under <pkg_root> after a successful commit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The ORT-side ModelPackageContext constructor now calls the standalone
library's public C API (ModelPackage_Open + walk via ModelComponent_*/
ModelVariant_*) instead of reaching into the legacy internal types
(model_package::ParsePackage, model_package_internal.h). The ORT-internal
C++ types (VariantInfo, ComponentInfo, ModelPackageInfo) are populated
from the C handles; the rest of ORT's surface is unchanged.
The 'ort' executor_info namespace (§5.3) is parsed in ORT, not the library:
model_file (relative to variant_directory), session_options, provider_options,
external_data (path or sha256: URI, resolved via ModelPackage_ResolveAssetUri).
variant additional_metadata feeds consumer_metadata.
Legacy library code removed: src/api.cc, src/parser.{h,cc},
src/model_package_internal.h. model_package_api.h trimmed to the shared
core types (export macro, ModelPackageStatus opaque, ModelPackageErrorCode).
The status helpers ModelPackage_GetErrorMessage/GetErrorCode/ReleaseStatus
are gone; only the new ModelPackageStatus_Message/Code/Release survive.
cmake/onnxruntime_session.cmake: drop now-unused include of
model_package/src/ and refresh the integration comment. ORT only needs
the public include/ directory.
All 85 standalone library tests still pass; onnxruntime_session links clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the opaque ModelComponent / ModelVariant handles and their query getters with flat POD structs (ModelComponentInfo, ModelVariantInfo, ModelExecutorInfoEntry, ModelSharedAssetInfo) reached by walking the tree returned from ModelPackage_Info(). The package owns a lazily built view cache that is dropped on any mutation; helper functions ModelPackage_FindComponent / ModelComponentInfo_FindVariant / ModelVariantInfo_FindExecutorInfo provide ergonomic by-name lookup. Update the ORT consumer (model_package_context.cc) to walk the new tree and add a variant.json fallback: if the manifest's variant entry has no executor_info["ort"], we now read "<variant_directory>/variant.json" if present so callers can author manifests without inline ORT config. Rename the internal namespace model_package_v2 back to model_package now that the original code path is gone, and strip references to the design doc / phase numbers / 'legacy' / 'v2' from comments throughout the library and its tests. Standalone library: all five test binaries (ort_json, inspection, asset_hashing, authoring, commit) build and pass on CPU. ORT: libonnxruntime_session.a builds cleanly against the new API. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Inline executor_info is an executor-specific contract; the library has no business asserting that a variant_directory must exist on disk at parse or SetVariant time. Executors resolve their own file references (shared assets, relative paths) at load time and will produce their own errors when files are missing. Forcing the check here added authoring friction (could not build a complete package in memory and commit once) for no library-level guarantee. Removed the parse-time error path and the corresponding test; updated the ModelPackage_SetVariant doc to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
variant_directory: - Existence is now required if and only if the field is explicitly declared in the variant body. The inferred default (variant_name under component_dir) remains allowed to be missing, with no eager check. - This catches "you declared a path, but the directory is not there" while keeping the library out of executor-specific payload validation. - Updated test_validate_asset_reach_flags_unknown_uri to mkdir the declared variant_directory ahead of SetVariant. Vacuum -> Prune: - More idiomatic verb (matches git/docker/npm). Renamed the public API (ModelPackage_Prune), the implementation file, the header section, and the standalone tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prune previously only swept unreferenced shared_assets/ entries. Extend it to also clean up directories that the library itself removed from the live tree via RemoveVariant, RemoveComponent, SetVariant (replace), or SetComponentExternal (re-point), so users don't have to manage on-disk cleanup after authoring edits. The library never walks package_root looking for unknown content. Instead, each mutation that drops a directory pushes the prior resolved path onto an explicit pending list on the ModelPackage, and Prune sweeps that list with four guards: inside package_root, still exists, not currently referenced (or an ancestor of any currently live dir), and past the existing prune grace window. Components are swept before variants so a single component_dir removal reclaims its child variant dirs in one call. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ap load When the selected variant declares external_data as a shared asset, route the resolved folder through kOrtSessionOptionsModelExternalInitializersFileFolderPath so ORT can locate the data file even when it does not live next to model.onnx. That config key is only honored by the buffer overload of Session::Load (model_location_ is set on path-load and shortcuts the hint). To preserve mmap-style behavior, gate on external_data presence: when set, clone the session options, add the folder hint, mmap the .onnx file via Env::Default and hand the buffer to CreateSessionAndLoadSingleModelImpl, then release the mmap. Otherwise keep today's path-load behavior so non-external models are unchanged. Add ModelPackageComponentContext::GetSelectedVariantExternalDataFolder accessor (cached) that surfaces shared_files["external_data"] populated by the manifest resolution step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Trim verbose multi-line comments across recent edits in commit_prune_validate.cc, manifest_parser.cc, and model_package_impl.h. Update the Prune docstring in the public header to reflect that it now also reclaims tracked orphan variant and component directories alongside shared_assets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…a_folder_path The shared_files map on VariantModelInfo was only ever populated with a single 'external_data' entry and read back in one place. Replace it with an explicit optional<string> external_data_folder_path field on the struct, which makes the data flow obvious and removes the redundant caching in ModelPackageComponentContext. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…or_info paths through it
ORT-side parsing of executor_info.ort.model_file and external_data was joining
paths against the variant folder by hand, skipping the portable/installed
confinement and '..' rejection that the rest of the library enforces, and only
external_data understood sha256: URIs (and only as a bare folder).
Add a single library primitive that handles every accepted form of a string
reference inside a model package:
- bare 'sha256:<hex>' -> shared-asset folder
- 'sha256:<hex>/sub/path' -> file or subdir inside an asset folder
(tail resolved with portable confinement
under the asset folder)
- relative path -> joined against base_dir (or package_root
when base_dir is null) under package
portable/installed semantics
- absolute path / '..' segments -> only allowed in installed layout
Switch the ORT model_package context to call ModelPackage_ResolveStringRef for
both model_file and external_data so they now uniformly accept all of the
above and inherit the same confinement rules. Errors surface the underlying
status message.
Add the helper TrySplitAssetUriPrefix on path_resolver to detect the
'sha256:<hex>[/<tail>]' form.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s==nullptr When the caller passes a null OrtSessionOptions and the selected variant does not declare external_data, options_to_use stayed null through to the RebuildProviderListForSession and LogTelemetry calls that dereference it. Synthesize a default OrtSessionOptions in that branch so options_to_use is always non-null, and drop the now-redundant null guard in the policy telemetry branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The library is the single source of truth for variant configuration via manifest.json + executor_info. The variant.json shorthand was an ORT-side legacy convention that read the file with raw std::ifstream, bypassing ModelPackage path resolution and producing two divergent code paths for the same logical config. Remove it; callers must declare executor_info in the manifest (inline or as an external file). Also fixes a stale error message referring to a variant.json descriptor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GetVariantEpCompatibility returns a Status that was being swallowed; an unknown component or variant returned a null ep with a success status. Forward the status as an OrtStatus when not OK so callers can distinguish "variant has no ep declared" from "component/variant not found". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously the view-cache build path (a const function) silently loaded and parsed external executor_info files, swallowing any I/O or schema errors and producing an empty body instead. Move the resolution into a new non-const RefreshExecutorInfoCache called once at Open (strict) and after every PostMutate (lenient: allows authoring SetExecutorInfoExternal to record a path before the file exists). The view cache now just maps pre-resolved strings into ABI structs with no I/O. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…DirName helpers
Replace three hand-rolled 'sha256-' + hex concatenations and one
'rfind("sha256-", 0)' parse with named helpers in path_resolver. Keeps
the on-disk naming convention in one place.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…fo / layout mutations MutateExecutorInfo and SetLayout cannot change uses_assets references nor shared_assets entries, so the shared-asset rescan is wasted work. Falls in line with SetMetadata and SetAdditionalMetadata which already passed refresh_assets=false. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Paths recorded onto pending_orphan_{variant,component}_dirs by Record*
calls were orphaned by an in-session mutation: there is no concurrent
writer to protect against, so making the user wait kPruneGrace before
the next Prune actually removes them is just confusing. The grace
window is still applied to the shared_assets sweep, which discovers
candidates fresh from disk.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ses_assets AddSharedAsset(copy_in=true) without any uses_assets reference produces a pending copy that has nothing referencing it. Previously commit would silently materialize it; now both the in-place and dest_root commits fail with ERR_STATE so the author notices the missing reference instead of shipping an orphan asset. Existing tests updated to add the reference; new negative test covers both commit paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dest_root commit path already rehashes the staged copy via ComputeDirectoryAssetUri and rejects mismatches (commit_prune_validate ~line 420). Add an explicit test that tampers with a landed sha256-<hex>/ directory between in-place commit and dest_root commit, confirming the mismatch is caught so the source-trust behavior cannot regress silently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
No consumer (ORT internals or integration tests) ever called OrtJson_*, so the opaque-handle DOM was only paying its cost in header bloat, build time, and review friction. Delete the public header, the implementation, and its dedicated unit test; clean up the doc strings that referenced it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ptions The captured OrtSessionOptions from CreateModelPackageOptionsFromSessionOptions only drives variant selection and EP discovery. The default-path session is built from a fresh OrtSessionOptions plus variant-metadata merge, NOT from the captured options. Update the doc to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
'ns' clashed with a stdlib convention (std::ns aliases are common) and the field meaning was unclear at the call site. Rename to namespace_key across the public C header, the impl, and the FindExecutorInfo parameter. No internal consumers (ORT, integration tests) referenced .ns directly, so the rename is contained. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous doc said 'remains valid until the asset is removed or the package is closed', but the pointer comes from either a vector-owned string_cache or an unordered_map key — both of which can be invalidated by the next mutation (PostMutate rebuilds shared_assets; the pending_shared_asset_copies map can rehash). Narrow the contract to 'until next mutation' so callers copy when they need to keep the value. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The old README documented an API surface that no longer exists (ModelPackage_CreateContext, ModelPackage_GetComponentCount, ...) and a directory layout from an earlier design (metadata.json + variant.json files per variant). Replace with a description of the current Open/Author/Commit + read-tree surface, the lifetime contract, the opaque-to-us boundaries (variant selection, executor_info payloads), and an accurate on-disk layout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dest_root commit path unconditionally ran fs::create_directories on <dest_root>/shared_assets/ before figuring out whether any assets were actually going in it. Packages that use no shared assets at all (e.g. a GenAI-style component that lays out its own files inside variant_directory) ended up with a stray empty folder on disk. Open / Load / Validate / Prune already tolerate the folder being absent, and the in-place commit path is already gated by pending_shared_asset_copies being non-empty. Gate the dest_root mkdir the same way. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rewrite model_package/README.md as a full reference for the package format: on-disk layout, portable vs installed, manifest/component/ variant schemas with field tables, shared-asset hashing (file names + contents), path resolution rules, C API tour, commit/prune/validate. - Add onnxruntime/core/session/model_package/README.md documenting ORT's consumer side: executor_info["ort"] schema (model_file, external_data, session_options, provider_options) with inline+external forms, variant selection algorithm (EP intent capture + ValidateCompiledModelCompat scoring), CreateSession session-options precedence, and C/Python API examples. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Ort::ModelPackageContext / Ort::ModelPackageOptions C++ RAII wrappers were removed when the model package surface moved onto the experimental C API. Drop the two CxxWrappers_* tests that still referenced them; their coverage is fully replicated by the existing function-table tests in the same file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
unistd.h and fcntl.h were included unconditionally in commit_prune_validate.cc even though the POSIX symbols they define (open, fsync, close, O_RDONLY, O_DIRECTORY, errno) are only used inside FsyncPath's non-Windows branch. Wrap the includes in #ifndef _WIN32 so MSVC stops failing with C1083. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
RandomSuffix() was casting a uint64_t to unsigned long and printing with %016lx. On Windows (LLP64) unsigned long is 32-bit, so the cast silently truncated half the entropy. Switch to unsigned long long and %016llx so the full 64-bit value is preserved on every platform. Apply the same fix to the matching helpers in the standalone test suites (not in the ORT Windows CI path today, but kept consistent for the time the tests are enabled). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-redesign # Conflicts: # include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc # onnxruntime/test/autoep/test_model_package.cc
There was a problem hiding this comment.
Pull request overview
This PR expands the standalone model_package/ library from read-only inspection into a full authoring + validation + commit workflow (with content-addressed shared assets and schema versioning), and updates ONNX Runtime’s model-package integration to consume the library via its public C API (including wiring external_data into session creation).
Changes:
- Replaces the old internal-only model package parser with a new public
model_package.hC API and in-library implementation split across focused translation units (parsing, path resolution, hashing, authoring, commit/prune/validate). - Updates ORT’s model package integration to open/inspect packages via the library C API and adds support for
external_databy switching to buffer-load + setting the external-initializers folder hint. - Adds standalone library tests (inspection/authoring/asset hashing/commit) and new integration documentation.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| onnxruntime/core/session/utils.cc | Adjusts model-package session creation to support external initializers via buffer-load + config entry. |
| onnxruntime/core/session/model_package/README.md | New documentation for ORT-side consumption, selection, and experimental API usage. |
| onnxruntime/core/session/model_package/model_package_context.h | Extends ORT’s internal variant model info with resolved external initializer folder path. |
| onnxruntime/core/session/model_package/model_package_context.cc | Switches ORT integration to the model_package public C API; parses executor_info["ort"]; resolves model/external_data refs. |
| onnxruntime/core/session/model_package_api.cc | Tightens error/out-param behavior for experimental model package API getters. |
| model_package/tests/test_inspection.cc | New standalone inspection API tests. |
| model_package/tests/test_commit.cc | New commit/prune/validate tests (in-place + dest_root flows). |
| model_package/tests/test_authoring.cc | New authoring/mutation API tests. |
| model_package/tests/test_asset_hashing.cc | New SHA-256 + directory Merkle-hash tests for shared assets. |
| model_package/src/status_impl.h | New internal status representation shared across the library. |
| model_package/src/sha256.h | New minimal SHA-256 interface for content-addressed assets. |
| model_package/src/sha256.cc | New SHA-256 implementation (used for directory hashing and asset validation). |
| model_package/src/path_resolver.h | New path resolution + confinement helpers and sha256 URI helpers. |
| model_package/src/path_resolver.cc | Implements portable/installed path rules and sha256 URI parsing. |
| model_package/src/model_package_impl.h | Defines the in-memory package representation and cached Info view materialization. |
| model_package/src/model_package_impl.cc | Implements lifecycle, info tree, lookups, string ref resolution, and hashing API. |
| model_package/src/manifest_parser.h | Declares manifest/component/variant parsing + cache refresh helpers. |
| model_package/src/manifest_parser.cc | Implements manifest/component/variant parsing into the in-memory model. |
| model_package/src/authoring.cc | Implements mutation APIs (set/remove components/variants/executor_info, shared assets, metadata). |
| model_package/src/asset_hasher.h | Declares directory Merkle-hash algorithm for canonical asset URIs. |
| model_package/src/asset_hasher.cc | Implements directory hashing rules (sorted manifest text, reject symlinks, posix paths). |
| model_package/src/commit_prune_validate.cc | Implements commit (“preserve”/“dense”), prune, and validate workflows. |
| model_package/src/parser.h | Deletes old internal parser header (superseded by new library). |
| model_package/src/parser.cc | Deletes old internal parser implementation (superseded by new library). |
| model_package/src/model_package_internal.h | Deletes old internal type definitions (replaced by new in-memory model). |
| model_package/src/api.cc | Deletes old standalone API implementation (replaced by new C API surface). |
| model_package/README.md | Major documentation expansion for layout/schema/assets/path resolution/commit-prune-validate. |
| model_package/include/model_package.h | New public API header defining lifecycle/inspection/authoring/commit/prune/validate. |
| model_package/include/model_package_api.h | Refactors shared types (export macro, status handle, stable error enum). |
| model_package/CMakeLists.txt | Updates sources/installs new headers and adds standalone tests wiring. |
| cmake/onnxruntime_session.cmake | Updates ORT build integration to include only the model_package public headers and use the public C API. |
| effective.allow_external_paths = false; | ||
| effective.follow_symlinks = true; | ||
| effective.strict_unknown_fields = true; | ||
| if (opts) { |
There was a problem hiding this comment.
abi_version is stored in every struct but never validated. If a caller compiled against a future v2 header passes abi_version = 2 (where field semantics or layout may have changed), this code silently proceeds with v1 interpretation, this might cause issue.
Should we gate on abi_version before using the struct?
There was a problem hiding this comment.
The structs no longer carry an abi_version (or struct_size) field. The model package library is consumed as source — each consumer compiles it into its own binary — so there is no separately-built binary boundary to version, and nothing to gate at runtime. On-disk compatibility is governed solely by schema_version ("major.minor"): the library accepts any package whose major is within its supported range, and consumers branch on schema_version_major / schema_version_minor to know which optional fields a package may carry. See the README "Schema versioning and source distribution" section.
| effective.allow_external_paths = false; | ||
| effective.follow_symlinks = true; | ||
| effective.strict_unknown_fields = true; | ||
| if (opts) { |
There was a problem hiding this comment.
abi_version is documented as 1 in every struct's doc comment, but there's no named constant for it. Callers have to hardcode the literal 1 , and the library has no single source of truth to validate against.
Consider adding a define:
#define MODEL_PACKAGE_ABI_VERSION 1
There was a problem hiding this comment.
abi_version has been removed entirely rather than turned into a named constant. Since the library ships as source with no binary boundary, an ABI-version macro would be vestigial (the same reasoning that removed struct_size). The only compatibility contract that remains is schema_version (major.minor), which is what consumers key off of.
| } | ||
|
|
||
| const ModelVariantInfo* ModelComponentInfo_FindVariant(const ModelComponentInfo* comp, | ||
| const char* name) { |
There was a problem hiding this comment.
Same concern here.
It should check abi_version of the ModelComponentInfo struct first, otherwise, consider the scenario where library/api is newer and consumer/caller is older, as per implementation of this function, it might return corrupted data.
There was a problem hiding this comment.
This accessor no longer exists — collections are plain array members again (components / num_components, etc.), and there is no abi_version or struct_size. With source distribution there is a single struct definition per build, so the newer-library / older-caller layout skew that motivated a check cannot arise. A breaking change to the on-disk data contract is instead carried by a schema_version major bump, which the parser gates on.
ModelPackageContext keeps the model_package handle open for its lifetime and exposes ResolveStringRef, which forwards to the model_package library's resolver. It handles sha256: content-addressed shared-asset references (honoring manifest overrides) and plain relative paths resolved against a base directory, with the resolved path cached for C-API pointer lifetime. The experimental C API gains OrtModelPackageApi_ModelPackage_ResolveStringRef so consumers can resolve package path references without reopening the package. autoep tests cover sha256 directory and tail resolution, relative-path resolution, and rejection of an undeclared asset. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ResolvePath runs the portable-layout confinement check whether or not the leaf exists (when a package_root is set), and uses weakly_canonical for a missing leaf so symlinks in the existing prefix are resolved. This closes a gap where a not-yet-created path could escape package_root through a symlinked prefix. The check is skipped when package_root is empty (in-memory authoring before the package is anchored to a directory). - CheckPortableConfinement only rejects an absolute candidate whose first relative component is '..', instead of any dot-prefixed name, so in-root hidden paths like '.hidden/component.json' are no longer wrongly rejected. - Add MODEL_PACKAGE_ABI_VERSION as the single source of truth for the struct abi_version fields and use it at every assignment site. - test_commit includes <sstream> directly for std::ostringstream. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-redesign # Conflicts: # onnxruntime/test/autoep/test_model_package.cc
…s via accessors The on-disk schema_version is a "<major>.<minor>" string. The library accepts any package whose major is in its supported range and any minor; evolution within a major is additive, so a single parser reads every minor and tolerates unknown fields from a newer minor. The schema version is validated up front, before component parsing. ModelPackageInfo exposes schema_version_major and schema_version_minor. The read API no longer exposes collections as raw arrays. Components, variants, shared assets, and executor infos are reached through count + index accessors, so the library owns the element stride and can append fields to the element structs without breaking a compiled consumer. Each element with children is stored as a view whose first member is the public struct, so an accessor recovers the children from the public pointer. Struct compatibility rests on struct_size plus an append-only layout enforced by static_asserts on field offsets; breaking changes are carried by the library SOVERSION. The per-struct abi_version field is removed. ModelPackage_Open validates a minimum options struct_size before reading caller fields. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The model package library is compiled into each consumer's own binary, so its POD structs have no binary boundary. Drop the struct_size/SOVERSION/static_assert ABI machinery and the count+index accessors, and read collections directly via array members (components/num_components, variants/num_variants, executor_infos, shared_assets). Compatibility is governed solely by schema_version (major.minor). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a "Versioning and compatibility" section to the model package README covering source distribution (no published shared library, hence no ABI machinery), the major.minor schema_version contract, what the parser enforces (unsupported major rejected, newer minor tolerated), and how the supported major range lets a breaking format change land without invalidating already published packages or forcing consumers to upgrade in lockstep. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a README subsection clarifying that the structs carry a single definition per build: an old-major package exists only as on-disk JSON, so reconciling it is a parse-time job. Document the superset/newest struct shape, parse-time normalization of older majors, nullable fields plus schema_version_major branching for non-migratable changes, and per-major typed structs as the escape hatch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reject drive-rooted paths (e.g. Windows "C:rel") alongside absolute paths in portable layout, since has_root_name() paths are not is_absolute() but still escape confinement. Error in GetSelectedVariantFilePath when the selected variant's ort entry has no model_file, instead of returning an empty path. Note allow_external_paths in the ResolveStringRef doc, align the Prune doc with its actual behavior (it never removes content-addressed shared-asset dirs), and clean up an exploratory comment in the hashing test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description
Builds out the standalone
model_package/C library so a single librarycovers the full lifecycle of an ONNX Runtime model package: inspection,
authoring, content-addressed shared assets, commit, prune, and validation.
The library remains free of any dependency on ONNX Runtime itself.
Public C API (
model_package/include/model_package.h)ModelPackage_Open/ModelPackage_New/ModelPackage_Close,with
ModelPackageOpenOptionscontrolling external-path access, symlinkfollowing, and strict unknown-field handling.
ModelPackageInfotree (ModelPackage_Info) plusby-name lookups for components, variants, and per-namespace
executor_infoentries, and round-trip JSON getters that preservefields unknown to the current build.
ModelPackage_ResolveStringRefimplements the package'sresolution rules — relative paths anchored at a base directory,
sha256:<hex>[/sub/path]for shared-asset content, and portable vsinstalled confinement (absolute paths and
..segments only allowedunder
layout: "installed").ModelPackage_AddSharedAsset(with reproducible-build URI check and an optional
copy_instagingmode),
ModelPackage_RemoveSharedAsset, andModelPackage_ResolveAssetUri. Assets under<package_root>/shared_assets/are auto-discovered atOpen.per-namespace executor_info setters (inline and external), and
package-level metadata / layout /
additional_metadatasetters.Mutations invalidate cached pointers in the mutated scope and its
descendants.
ModelPackage_Commitwrites the in-memorymodel to disk either in place or to a fresh
dest_root("save as"),with
PRESERVEorDENSEwrite modes;ModelPackage_Prunereclaimsunreferenced files under
shared_assets/and tracked orphanvariant/component directories left behind by removals; and
ModelPackage_Validateruns schema, path-reachability, asset-rehash,and unknown-field checks and returns a JSON report.
ModelPackageStatus*with a stable additiveModelPackageErrorCodeenum (IO, schema, version, path confinement,asset missing, asset hash mismatch, not found, invalid arg, state).
Internal layout
The implementation is split into focused translation units:
manifest_parser,model_package_impl,authoring,commit_prune_validate,path_resolver,asset_hasher, and anin-tree
sha256. Shared error plumbing lives instatus_impl.h.ONNX Runtime integration (
onnxruntime/core/session/model_package/)The ORT-side glue is wired onto the library through the C inspection
and path-resolution entry points.
model_package_contextnow translatesthe library's info tree into ORT-internal structs and parses the
executor_info["ort"]payload (model_file,external_data,session_options,provider_options). When a variant declaresexternal_data,CreateSessionForModelPackageloads the model from amemory mapping and passes the resolved folder to the session via
kOrtSessionOptionsModelExternalInitializersFileFolderPath, so externalinitializers — including those backed by a shared asset — are picked up
at
Initializetime.The experimental
OrtModelPackageApi_*_SinceV28C entries introduced in#28990 are unchanged.
Documentation and tests
model_package/README.mddocuments the on-disk layout, manifest andcomponent schemas, shared-asset rules, path resolution, the authoring
flow, and commit / prune / validate semantics.
onnxruntime/core/session/model_package/README.mddocuments the ORTconsumer-side glue: the
executor_info["ort"]schema, the variantselection algorithm, the session-creation contract, and the registered
experimental C entries.
commit (
model_package/tests/). The ORT integration tests inonnxruntime/test/autoep/test_model_package.ccare reworked againstthe current C API surface.
Motivation and Context
ORT needs a single library that owns the model package format end to
end — not just reading it, but producing it, validating it, and
maintaining it on disk with content-addressed shared assets.
Consolidating this behind one C API lets ORT, publisher tooling, and
third-party consumers share the same parser, path-resolution rules, and
on-disk invariants without each reimplementing them, and keeps the
library independent of the ORT session runtime.