Ollama/OllamaCliProgress/OllamaCliProgress.cscsharp

Documentation

OllamaCliProgress

Console progress reporting during model pulls and long operations.

OllamaCliProgress.cs

  • Formats download / wait status for terminal output while Ollama starts or pulls models.
using System.Text;using System.Text.Json;using System.Text.RegularExpressions;namespace Agent.Core.Ollama;/// <summary>/// Reads <c>ollama pull</c> stdout/stderr and exposes the latest status for single-line logging./// </summary>internal sealed class OllamaCliProgressTracker(string model){    private readonly object _sync = new();    private string _lastStatus = "starting…";    private int? _percent;    public string Model { get; } = model;    public void Update(string raw)    {        string line = StripAnsi(raw.Trim());        if (string.IsNullOrWhiteSpace(line))        {            return;        }        lock (_sync)        {            if (TryParsePullJson(line, out string? status, out int? percent))            {                _lastStatus = status ?? line;                if (percent.HasValue)                {                    _percent = percent;                }                return;            }            _lastStatus = line;        }    }    public string GetDisplayMessage(TimeSpan elapsed, TimeSpan remaining)    {        lock (_sync)        {            if (_percent is { } pct)            {                return $"Pulling '{Model}': {_lastStatus} ({pct}%) — {elapsed:mm\\:ss} elapsed, {remaining:mm\\:ss} left";            }            return $"Pulling '{Model}': {_lastStatus} — {elapsed:mm\\:ss} elapsed, {remaining:mm\\:ss} left";        }    }    private static bool TryParsePullJson(string line, out string? status, out int? percent)    {        status = null;        percent = null;        if (!line.StartsWith('{'))        {            return false;        }        try        {            using JsonDocument doc = JsonDocument.Parse(line);            JsonElement root = doc.RootElement;            if (root.TryGetProperty("status", out JsonElement statusEl))            {                status = statusEl.GetString();            }            if (root.TryGetProperty("completed", out JsonElement doneEl) &&                root.TryGetProperty("total", out JsonElement totalEl) &&                totalEl.TryGetInt64(out long total) &&                total > 0 &&                doneEl.TryGetInt64(out long done))            {                percent = (int)(done * 100 / total);            }            return status is not null || percent.HasValue;        }        catch        {            return false;        }    }    private static string StripAnsi(string text) =>        Regex.Replace(text, @"\x1b\[[0-9;]*[a-zA-Z]", string.Empty);}internal static class OllamaCliStreamReader{    public static async Task PumpAsync(TextReader reader, OllamaCliProgressTracker tracker, CancellationToken cancellationToken = default)    {        var buffer = new char[512];        var segment = new StringBuilder();        while (!cancellationToken.IsCancellationRequested)        {            int read = await reader.ReadAsync(buffer, cancellationToken);            if (read <= 0)            {                break;            }            for (int i = 0; i < read; i++)            {                char c = buffer[i];                if (c is '\r' or '\n')                {                    Flush(segment, tracker);                }                else                {                    segment.Append(c);                }            }        }        Flush(segment, tracker);    }    private static void Flush(StringBuilder segment, OllamaCliProgressTracker tracker)    {        if (segment.Length == 0)        {            return;        }        tracker.Update(segment.ToString());        segment.Clear();    }}