Skip to content

Per-EP Download Progress Reporting#566

Open
bmehta001 wants to merge 15 commits intomainfrom
bhamehta/per-ep-progress-sample
Open

Per-EP Download Progress Reporting#566
bmehta001 wants to merge 15 commits intomainfrom
bhamehta/per-ep-progress-sample

Conversation

@bmehta001
Copy link
Copy Markdown
Contributor

Adds per-execution-provider download progress reporting across all SDKs and samples. Previously, EP downloads
only reported aggregate progress (e.g., "1 of 4 done"). Now each EP reports individual download progress
(0–100%), enabling real-time progress bars per EP.

Changes

SDK — C# (sdk/cs/)

  • FoundryLocalManager.cs: Added EnsureEpsDownloadedAsync overload accepting Action<string, double> callback for
    per-EP progress. Parses "epName|percent" wire format from native interop.
  • EpInfo.cs: New EpInfo type exposing EP name and registration status from DiscoverEps().

SDK — JavaScript (sdk/js/)

  • foundryLocalManager.ts: Added ensureEpsDownloaded(names, progressCallback) and discoverEps() methods.
  • install-standard.cjs / install-winml.cjs: Updated Core package version to 0.9.0-dev-20260331T004032-a768b6a.

SDK — Python (sdk/python/)

  • foundry_local_manager.py: Added ensure_eps_downloaded() with per-EP progress callback and discover_eps().
  • requirements.txt: Updated Core version to 0.9.0.dev20260331004032.

SDK — Rust (sdk/rust/)

  • foundry_local_manager.rs: Added ensure_eps_downloaded() with per-EP progress callback and discover_eps().
  • types.rs: Added EpInfo struct.
  • build.rs: Updated CORE_VERSION.

Samples

  • samples/cs/GettingStarted/Program.cs: Replaced RunWithSpinner EP call with discover → display → per-EP
    progress download. Guarded for empty EP arrays.
  • samples/js/native-chat-completions/app.js: Added EP discovery and per-EP progress block with aligned columns
    and progress bars. Guarded for empty EP arrays.
  • Directory.Packages.props: Updated package versions.

bmehta001 and others added 13 commits March 19, 2026 14:58
- Enable ManualEpDownload, call ensureEpsDownloaded with progress callback
- Render per-EP progress bars in terminal

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rogress

- Update JS/C#/Rust/Python SDKs: ensure_eps_downloaded -> download_and_register_eps
- Add progress callback support to Python SDK ensure_eps_downloaded()
- Python now parses 'epName|percent' streaming chunks like other SDKs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add discoverEps() to JS, C#, Rust, and Python SDKs (wraps discover_eps command)
- Add optional names parameter to ensureEpsDownloaded() in all SDKs
- Update JS sample to demonstrate: discover EPs -> download all with progress
- Update C# HelloFoundryLocalSdk sample with discover + selective download
- Add EpInfo type to C# and Rust SDKs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Track current EP name; emit newline when switching to preserve the
previous EP's completed progress bar. Each EP gets its own line.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pad EP names to the longest name so (registered:) and progress bars
start at the same column across all lines.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Point all SDKs and samples at the latest FoundryLocalCore packages
published to ORT-Nightly, which include per-EP progress callback
support with Action<string, double> on IEpBootstrapper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…a768b6a

- Streamline Program.cs: remove verbose comments and redundant code
- Update NuGet versions to 0.9.0-dev-20260331T004032-a768b6a
- Update JS SDK versions (standard + WinML)
- Update Rust CORE_VERSION
- Update Python requirements to 0.9.0.dev20260331004032

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- C#: wrap EP logic in eps.Length > 0 guard to avoid InvalidOperationException
- JS: wrap EP logic in eps.length > 0 guard to avoid Math.max(-Infinity)
- Replace nullable currentEp with empty string for cleaner null safety

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…gitignore

- Restore all original comments and code structure in C# and JS samples
- Only change EP-related code (discover + per-EP progress download)
- Guard empty EP arrays to avoid InvalidOperationException / Math.max(-Infinity)
- Remove package.json from tracking (not needed in repo)
- Remove copilot-instructions.md from gitignore

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- C# README: EP discovery and progress section, EpInfo in API ref table
- C# API docs: regenerated with xmldoc2md (new DiscoverEps, EnsureEpsDownloadedAsync overload, EpInfo)
- JS README: EP discovery and progress section in WinML section
- JS API docs: regenerated with typedoc (new discoverEps, ensureEpsDownloaded)
- Python README: EP discovery feature and usage section
- Rust README: EP discovery and progress section
- Fix AOT compat: use source-generated JsonSerializationContext for EpInfo[] deserialization
- copilot-instructions.md: add documentation checklist reminder

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 31, 2026 06:44
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
foundry-local Ready Ready Preview, Comment Mar 31, 2026 7:07am

Request Review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds per-execution-provider (EP) discovery and per-EP download progress reporting across C#, JS/TS, Python, and Rust SDKs, plus updated samples and package versions to a newer Core build.

Changes:

  • Added EP discovery APIs (discover_eps wrappers) and EpInfo types (C#/Rust) or equivalents (JS/Python).
  • Added per-EP progress callbacks for EP download/registration, using the "epName|percent" streaming wire format.
  • Updated Core package versions and refreshed SDK/sample documentation and examples.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
sdk/rust/src/types.rs Adds EpInfo struct to model EP discovery results.
sdk/rust/src/foundry_local_manager.rs Adds discover_eps and ensure_eps_downloaded APIs (with optional streaming progress).
sdk/rust/build.rs Bumps CORE_VERSION to a new dev build.
sdk/rust/README.md Documents EP discovery + per-EP progress usage.
sdk/python/src/foundry_local_manager.py Adds discover_eps() and extends ensure_eps_downloaded() with names + progress callback.
sdk/python/requirements.txt Updates foundry-local-core pinned dev version.
sdk/python/README.md Adds usage docs for EP discovery and per-EP progress.
sdk/js/src/foundryLocalManager.ts Adds discoverEps() and ensureEpsDownloaded() with streaming progress parsing.
sdk/js/script/install-winml.cjs Bumps Core.WinML package version used by install script.
sdk/js/script/install-standard.cjs Bumps Core package version used by install script.
sdk/js/docs/classes/FoundryLocalManager.md Adds API docs for discoverEps / ensureEpsDownloaded.
sdk/js/README.md Documents EP discovery + per-EP progress usage.
sdk/cs/src/FoundryLocalManager.cs Adds DiscoverEps() and overloads EnsureEpsDownloadedAsync with names + per-EP progress callback.
sdk/cs/src/EpInfo.cs Introduces EpInfo model for C# EP discovery.
sdk/cs/src/Detail/JsonSerializationContext.cs Adds source-gen serialization support for EpInfo[].
sdk/cs/docs/api/microsoft.ai.foundry.local.openaichatclient.md Updates generated API docs (includes new overload docs).
sdk/cs/docs/api/microsoft.ai.foundry.local.openai.responseformatextended.md Adds generated API doc page for ResponseFormatExtended.
sdk/cs/docs/api/microsoft.ai.foundry.local.modelinfo.md Updates generated docs for newly documented ModelInfo properties.
sdk/cs/docs/api/microsoft.ai.foundry.local.foundrylocalmanager.md Updates generated docs for DiscoverEps and new EnsureEpsDownloadedAsync signature.
sdk/cs/docs/api/microsoft.ai.foundry.local.epinfo.md Adds generated API doc page for EpInfo.
sdk/cs/docs/api/index.md Adds EpInfo and ResponseFormatExtended entries to API index.
sdk/cs/README.md Adds usage docs for EP discovery + per-EP progress and updates key types table.
samples/js/native-chat-completions/app.js Updates sample to discover EPs and render per-EP progress bars.
samples/cs/GettingStarted/src/HelloFoundryLocalSdk/Program.cs Updates sample to discover EPs and show per-EP progress during downloads.
samples/cs/GettingStarted/Directory.Packages.props Updates Foundry.Local and WinML package versions.
.github/copilot-instructions.md Adds repo guidance on EP progress format and package update workflow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

* Discovers the execution providers available for download and registration.
* @returns An array of EP info objects with name and registration status.
*/
public discoverEps(): { name: string; isRegistered: boolean }[] {
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The declared return type is { name, isRegistered }[], but the implementation returns the Core JSON as-is (which elsewhere in this PR is documented/used as { Name, IsRegistered }). This will cause runtime undefined values for consumers who follow the TypeScript type (ep.name) and can also break generated docs/examples consistency. Fix by either (a) changing the return type to match the actual JSON shape ({ Name: string; IsRegistered: boolean }[]) or (b) mapping the parsed objects to a camelCase shape before returning (and then update docs/samples accordingly).

Suggested change
public discoverEps(): { name: string; isRegistered: boolean }[] {
public discoverEps(): { Name: string; IsRegistered: boolean }[] {

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +48
const eps = manager.discoverEps();
eps.forEach(ep => {
console.log(` ${ep.Name} (registered: ${ep.IsRegistered})`);
});

// Download with per-EP progress reporting
const epNames = eps.map(ep => ep.Name);
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README examples/documentation treat discoverEps() results as { Name, IsRegistered }, but the TS signature currently declares { name, isRegistered }. Once the API shape is finalized, please align the README with the actual exported JS/TS API surface (and keep it consistent with the generated docs in sdk/js/docs/...).

Copilot uses AI. Check for mistakes.
});
```

`discoverEps()` returns an array of `{ Name, IsRegistered }` objects. The progress callback receives the EP name and a percentage (0–100) as each EP downloads.
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README examples/documentation treat discoverEps() results as { Name, IsRegistered }, but the TS signature currently declares { name, isRegistered }. Once the API shape is finalized, please align the README with the actual exported JS/TS API surface (and keep it consistent with the generated docs in sdk/js/docs/...).

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +83
let ep_names: Vec<&str> = eps.iter().map(|ep| ep.name.as_str()).collect();
manager.ensure_eps_downloaded(
Some(&ep_names),
Some(|name: &str, percent: f64| {
print!("\r {name}: {percent:.1}%");
std::io::Write::flush(&mut std::io::stdout()).ok();
}),
).await?;
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example won't compile against the Rust API added in this PR: ensure_eps_downloaded currently accepts Option<F> where F: FnMut(&str), documented as receiving raw \"name|percent\" chunks. Update the README to show parsing the raw chunk inside the callback, or change the Rust API to accept a typed callback (e.g., (name, percent)), and keep the README aligned with that choice.

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +170
/// If `progress` is provided, it receives raw `"name|percent"` strings for each
/// EP as the download proceeds.
pub async fn ensure_eps_downloaded<F>(
&self,
names: Option<&[&str]>,
progress: Option<F>,
) -> Result<()>
where
F: FnMut(&str) + Send + 'static,
{
let params = names.map(|n| json!({ "Names": n.join(",") }));
match progress {
Some(cb) => {
self.core
.execute_command_streaming_async(
"download_and_register_eps".into(),
params.as_ref(),
cb,
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other SDKs in this PR expose a per-EP progress callback as (ep_name, percent) while Rust exposes raw wire-format chunks. Consider parsing the \"name|percent\" format inside the Rust SDK and surfacing a typed callback (e.g., FnMut(Option<&str>, f64) or FnMut(&str, f64)), which makes the API consistent across languages and avoids duplicating parsing logic in every consumer. If you keep the raw format, the public docs/examples should consistently describe and demonstrate the raw chunk contract.

Suggested change
/// If `progress` is provided, it receives raw `"name|percent"` strings for each
/// EP as the download proceeds.
pub async fn ensure_eps_downloaded<F>(
&self,
names: Option<&[&str]>,
progress: Option<F>,
) -> Result<()>
where
F: FnMut(&str) + Send + 'static,
{
let params = names.map(|n| json!({ "Names": n.join(",") }));
match progress {
Some(cb) => {
self.core
.execute_command_streaming_async(
"download_and_register_eps".into(),
params.as_ref(),
cb,
/// If `progress` is provided, it receives parsed `(ep_name, percent)` updates for each
/// EP as the download proceeds. `ep_name` is `None` if the name is not present.
pub async fn ensure_eps_downloaded<F>(
&self,
names: Option<&[&str]>,
progress: Option<F>,
) -> Result<()>
where
F: FnMut(Option<&str>, f64) + Send + 'static,
{
let params = names.map(|n| json!({ "Names": n.join(",") }));
match progress {
Some(mut user_cb) => {
self.core
.execute_command_streaming_async(
"download_and_register_eps".into(),
params.as_ref(),
move |chunk: &str| {
if let Some((name_part, percent_part)) = chunk.split_once('|') {
if let Ok(percent) = percent_part.trim().parse::<f64>() {
let ep_name = if name_part.is_empty() {
None
} else {
Some(name_part)
};
user_cb(ep_name, percent);
}
}
},

Copilot uses AI. Check for mistakes.
Comment on lines +315 to +318
if (double.TryParse(progressString[(sepIndex + 1)..], out var percent))
{
progressCallback(string.IsNullOrEmpty(name) ? null : name, percent);
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parsing percent via double.TryParse(...) is culture-sensitive; on locales that use , for decimals, valid wire values like \"42.5\" may fail to parse and suppress progress updates. Use double.TryParse(..., NumberStyles.Float, CultureInfo.InvariantCulture, out var percent) (and add the corresponding using directives) so the callback works reliably regardless of current culture.

Copilot uses AI. Check for mistakes.

```csharp
public Task EnsureEpsDownloadedAsync(Nullable<CancellationToken> ct)
public Task EnsureEpsDownloadedAsync(IEnumerable<string> names, Action<string, double> progressCallback, Nullable<CancellationToken> ct)
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated API docs show non-optional names and progressCallback parameters, but the implementation signature in FoundryLocalManager.cs makes both optional (IEnumerable<string>? names = null, Action<string?, double>? progressCallback = null). Please regenerate/fix the docs so they accurately reflect optionality/nullability; otherwise consumers may treat optional parameters as required and miss the null name possibility.

Copilot uses AI. Check for mistakes.
Comment on lines +172 to +176
`names` [IEnumerable&lt;String&gt;](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerable-1)<br>
Optional list of EP names to download. If null or empty, all discoverable EPs are downloaded.

`progressCallback` [Action&lt;String, Double&gt;](https://docs.microsoft.com/en-us/dotnet/api/system.action-2)<br>
Optional callback receiving per-EP progress updates.
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated API docs show non-optional names and progressCallback parameters, but the implementation signature in FoundryLocalManager.cs makes both optional (IEnumerable<string>? names = null, Action<string?, double>? progressCallback = null). Please regenerate/fix the docs so they accurately reflect optionality/nullability; otherwise consumers may treat optional parameters as required and miss the null name possibility.

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +114
def _on_chunk(chunk: str):
sep_index = chunk.find('|')
if sep_index >= 0:
name = chunk[:sep_index] or None
try:
percent = float(chunk[sep_index + 1:])
except ValueError:
percent = 0.0
progress_callback(name, percent)
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

progress_callback is documented as receiving (ep_name: str, percent: float), but this implementation may pass None for ep_name. Either always pass a string (e.g., empty string) or update the docstring/type hints to indicate Optional[str] so the callback contract is consistent and predictable for typed users.

Copilot uses AI. Check for mistakes.
- JS: discoverEps() return type changed to { Name, IsRegistered } matching Core JSON
- Rust: parse wire format inside SDK; expose typed FnMut(&str, f64) callback
- C#: use CultureInfo.InvariantCulture for double.TryParse (locale safety)
- Python: pass empty string instead of None for ep_name in callback
- Regenerated JS (typedoc) and C# (xmldoc2md) API docs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
execute_command_streaming_async and execute_command_async take Option<Value>,
not Option<&Value>. Remove .as_ref() calls.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants