<?xml version="1.0" encoding="UTF-8" standalone="no"?><rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#" xmlns:georss="http://www.georss.org/georss" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:media="http://search.yahoo.com/mrss/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" version="2.0">

<channel>
	<title>El Bruno</title>
	<atom:link href="https://elbruno.com/feed/" rel="self" type="application/rss+xml"/>
	<link>https://elbruno.com</link>
	<description>Dev Advocating &#129361; @Microsoft</description>
	<lastBuildDate>Sun, 25 Oct 2026 20:36:25 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>http://wordpress.com/</generator>
<site xmlns="com-wordpress:feed-additions:1">19872796</site><cloud domain="elbruno.com" path="/?rsscloud=notify" port="80" protocol="http-post" registerProcedure=""/>
<source:cloud xmlns:source="https://source.scripting.com/">https://elbruno.com/?rsscloud=notify</source:cloud>
<image>
		<url>https://s0.wp.com/i/webclip.png</url>
		<title>El Bruno</title>
		<link>https://elbruno.com</link>
	</image>
	<atom:link href="https://elbruno.com/osd.xml" rel="search" title="El Bruno" type="application/opensearchdescription+xml"/>
	<atom:link href="https://elbruno.com/?pushpress=hub" rel="hub"/>
	<item>
		<title>C# + Ollama: Give Your App a Voice with Phonon</title>
		<link>https://elbruno.com/2026/10/25/c-ollama-give-your-app-a-voice-with-phonon/</link>
					<comments>https://elbruno.com/2026/10/25/c-ollama-give-your-app-a-voice-with-phonon/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Sun, 25 Oct 2026 20:35:08 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[Apple Silicon]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Microsoft.Extensions.AI]]></category>
		<category><![CDATA[NuGet Packages]]></category>
		<category><![CDATA[Ollama]]></category>
		<category><![CDATA[Phonon]]></category>
		<category><![CDATA[Text-to-Speech]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=43767</guid>

					<description><![CDATA[Hi! Apple Silicon users, please try this C# sample and let us know whether speech generation and playback work on your Mac! ElBruno.Speech 1.6.0-preview.5 keeps setup simple: call services.AddOllamaSpeech() for Microsoft.Extensions.AI.ITextToSpeechClient. It defaults to local Ollama and the phonon model. Let&#8217;s turn a sentence into a WAV file, with local inference handled by Ollama. Conceptual [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Apple Silicon users, please try this C# sample and let us know whether speech generation and playback work on your Mac!</strong></p>
</blockquote>



<p class="wp-block-paragraph"><strong>ElBruno.Speech 1.6.0-preview.5</strong> keeps setup simple: call <code>services.AddOllamaSpeech()</code> for <code>Microsoft.Extensions.AI.ITextToSpeechClient</code>. It defaults to local Ollama and the <code>phonon</code> model. Let&#8217;s turn a sentence into a WAV file, with local inference handled by Ollama.</p>



<figure class="wp-block-image size-large"><img width="1024" height="512" src="https://elbruno.com/wp-content/uploads/2026/10/image-7.png?w=1024" alt="" class="wp-image-43768" srcset="https://elbruno.com/wp-content/uploads/2026/10/image-7.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/10/image-7.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/10/image-7.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/10/image-7.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/10/image-7.png?w=1440 1440w, https://elbruno.com/wp-content/uploads/2026/10/image-7.png 1774w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><em>Conceptual flow, not a recording of generated audio. Image generated with AI.</em></p>



<h2 class="wp-block-heading">1. Get ready</h2>



<p class="wp-block-paragraph">Use .NET 8 or 10 and a supported <strong>Apple Silicon Mac running Ollama with MLX</strong>. Ollama&#8217;s speech announcement recommends version 0.40.11 or later.</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-shell"><div class="cm-line">ollama pull phonon</div><div class="cm-line">dotnet new console <span class="tok-propertyName">-n</span> HelloPhonon <span class="tok-propertyName">-f</span> net10.0</div><div class="cm-line"><span class="tok-variableName">cd</span> HelloPhonon</div><div class="cm-line">dotnet add package ElBruno.Speech.Tts.Ollama <span class="tok-propertyName">--version</span> <span class="tok-number">1</span>.6.0-preview.5</div><div class="cm-line">dotnet add package ElBruno.Speech.Audio <span class="tok-propertyName">--version</span> <span class="tok-number">1</span>.6.0-preview.5</div><div class="cm-line">dotnet add package Microsoft.Extensions.DependencyInjection <span class="tok-propertyName">--version</span> <span class="tok-number">10</span>.0.13</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">For .NET 8, replace <code>net10.0</code> with <code>net8.0</code>. Make sure Ollama is running.</p>



<p class="wp-block-paragraph"><strong>Windows friends:</strong> the C# client can call a supported remote Mac, but local Ollama speech currently requires MLX. Windows/Linux server support is pending upstream, with no promised date.</p>



<h2 class="wp-block-heading">2. Replace Program.cs</h2>



<p class="wp-block-paragraph">The MEAI speech API is experimental. The pragma below explicitly opts into it for this preview.</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-variableName">#pragma</span> <span class="tok-variableName">warning</span> <span class="tok-variableName">disable</span> <span class="tok-variableName">MEAI001</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">Speech</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">Speech</span>.<span class="tok-variableName">Audio</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">Speech</span>.<span class="tok-variableName">Tts</span>.<span class="tok-variableName">Ollama</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">Microsoft</span>.<span class="tok-variableName">Extensions</span>.<span class="tok-variableName">AI</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">Microsoft</span>.<span class="tok-variableName">Extensions</span>.<span class="tok-variableName">DependencyInjection</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">services</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ServiceCollection</span>();</div><div class="cm-line"><span class="tok-variableName">services</span>.<span class="tok-variableName">AddOllamaSpeech</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">provider</span> <span class="tok-operator">=</span> <span class="tok-variableName">services</span>.<span class="tok-variableName">BuildServiceProvider</span>();</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">speech</span> <span class="tok-operator">=</span> <span class="tok-variableName">provider</span>.<span class="tok-variableName">GetRequiredService</span><span class="tok-operator">&lt;</span><span class="tok-variableName">ITextToSpeechClient</span><span class="tok-operator">&gt;</span>();</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">pcm</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">MemoryStream</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">await</span> <span class="tok-keyword">foreach</span> (<span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">update</span> <span class="tok-keyword">in</span> <span class="tok-variableName">speech</span>.<span class="tok-variableName">GetStreamingAudioAsync</span>(</div><div class="cm-line">    <span class="tok-string">&quot;Hello from C sharp! My app has a voice thanks to Ollama Phonon.&quot;</span>,</div><div class="cm-line">    <span class="tok-keyword">new</span> <span class="tok-variableName">TextToSpeechOptions</span> { <span class="tok-variableName">AudioFormat</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;pcm&quot;</span> }))</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">foreach</span> (<span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">audio</span> <span class="tok-keyword">in</span> <span class="tok-variableName">update</span>.<span class="tok-variableName">Contents</span>.<span class="tok-variableName">OfType</span><span class="tok-operator">&lt;</span><span class="tok-variableName">DataContent</span><span class="tok-operator">&gt;</span>())</div><div class="cm-line">        <span class="tok-keyword">await</span> <span class="tok-variableName">pcm</span>.<span class="tok-variableName">WriteAsync</span>(<span class="tok-variableName">audio</span>.<span class="tok-variableName">Data</span>);</div><div class="cm-line">}</div><div class="cm-line"></div><div class="cm-line"><span class="tok-typeName">byte</span>[] <span class="tok-variableName">samples</span> <span class="tok-operator">=</span> <span class="tok-variableName">pcm</span>.<span class="tok-variableName">ToArray</span>();</div><div class="cm-line"><span class="tok-keyword">if</span> (<span class="tok-variableName">samples</span>.<span class="tok-variableName">Length</span> <span class="tok-operator">==</span> <span class="tok-number">0</span> <span class="tok-operator">||</span> <span class="tok-variableName">samples</span>.<span class="tok-variableName">Length</span> <span class="tok-operator">%</span> <span class="tok-number">2</span> <span class="tok-operator">!=</span> <span class="tok-number">0</span>)</div><div class="cm-line">    <span class="tok-keyword">throw</span> <span class="tok-keyword">new</span> <span class="tok-variableName">InvalidDataException</span>(<span class="tok-string">&quot;Expected complete PCM16 audio samples.&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">WavWriter</span>.<span class="tok-variableName">Write</span>(<span class="tok-string">&quot;hello.wav&quot;</span>,</div><div class="cm-line">    <span class="tok-keyword">new</span> <span class="tok-variableName">AudioFormat</span>(<span class="tok-number">24000</span>, <span class="tok-number">1</span>, <span class="tok-variableName">AudioSampleFormat</span>.<span class="tok-typeName">Int16</span>), <span class="tok-variableName">samples</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Saved: {Path.GetFullPath(&quot;</span><span class="tok-variableName">hello</span>.<span class="tok-variableName">wav</span><span class="tok-string">&quot;)}&quot;</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Run it:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-shell"><div class="cm-line">dotnet run</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">On a supported Mac, listen with:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-shell"><div class="cm-line">afplay hello.wav</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The client receives PCM chunks incrementally. This short example buffers them, then writes a WAV with the final sizes: <strong>24 kHz, mono, signed 16-bit PCM</strong>. It does not play audio while streaming.</p>



<p class="wp-block-paragraph">No configuration is required for local Ollama: the defaults are <code>http://localhost:11434/</code>, model <code>phonon</code>, the model&#8217;s default voice, and WAV output. This example requests PCM per call for streaming. Using a remote Mac? Replace the registration with <code>services.AddOllamaSpeech(new OllamaSpeechOptions { Endpoint = new Uri("http://YOUR-MAC:11434/") });</code>. The library resolves the speech route automatically. Keep that server on a trusted network; this example does not configure authentication or expose it.</p>



<h2 class="wp-block-heading">3. Try the complete demo</h2>



<p class="wp-block-paragraph">The <a href="https://github.com/elbruno/ElBruno.Speech/tree/v1.6.0-preview.5/src/samples/OllamaSpeechDemo">full C# sample</a> also saves complete WAV and streamed PCM/WAV files, validates their format, rejects silent output, and supports optional playback.</p>



<p class="wp-block-paragraph"><strong>Honest status:</strong> 49 synthetic provider/sample test cases pass on each of .NET 8 and .NET 10, including no-options registration, default request values, and URL normalization for complete and streaming requests. My Windows host could not load Phonon because MLX was unavailable, so I have <strong>not generated or listened to real Phonon speech yet</strong>. Automated checks are not proof of pronunciation or sound quality.</p>



<figure class="wp-block-image size-large"><img width="1024" height="512" src="https://elbruno.com/wp-content/uploads/2026/10/image-8.png?w=1024" alt="" class="wp-image-43770" srcset="https://elbruno.com/wp-content/uploads/2026/10/image-8.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/10/image-8.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/10/image-8.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/10/image-8.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/10/image-8.png?w=1440 1440w, https://elbruno.com/wp-content/uploads/2026/10/image-8.png 1774w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><em>Community testing invitation, not proof of working audio. Image generated with AI.</em></p>



<p class="wp-block-paragraph">If you test it, share your Mac chip, macOS and Ollama versions, whether <code>hello.wav</code> plays, and how it sounds in <a href="https://github.com/elbruno/ElBruno.Speech/issues/106">issue #106</a>. Please include any error message, but no secrets or private audio.</p>



<p class="wp-block-paragraph"><a href="https://www.nuget.org/packages/ElBruno.Speech.Tts.Ollama/1.6.0-preview.5">NuGet package</a> | <a href="https://github.com/elbruno/ElBruno.Speech/releases/tag/v1.6.0-preview.5">Release notes</a> | <a href="https://docs.ollama.com/capabilities/speech">Ollama speech docs </a></p>



<p class="wp-block-paragraph"></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/10/25/c-ollama-give-your-app-a-voice-with-phonon/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">43767</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/10/image-7.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/10/image-7.png">
			<media:title type="html">image</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/10/image-7.png?w=1024"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/10/image-8.png?w=1024"/>
	</item>
		<item>
		<title>Which device is on this COM port? A tiny .NET 10 utility</title>
		<link>https://elbruno.com/2026/10/25/which-device-is-on-this-com-port-a-tiny-net-10-utility/</link>
					<comments>https://elbruno.com/2026/10/25/which-device-is-on-this-com-port-a-tiny-net-10-utility/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Sun, 25 Oct 2026 15:59:01 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[COM]]></category>
		<category><![CDATA[COM Ports]]></category>
		<category><![CDATA[Device]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[StackChan]]></category>
		<category><![CDATA[Windows]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=43753</guid>

					<description><![CDATA[Hi! While building the USB demo for the upcoming New Year, New Bot series, I had two small robots connected to one Windows PC. Windows showed several COM ports, but the names did not tell me which cable belonged to which robot. That matters before connecting to a device, and especially before choosing a firmware [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img width="1024" height="576" src="https://elbruno.com/wp-content/uploads/2026/10/image-3.png?w=1024" alt="Two StackChan robots beside their retail box. One screen shows a colorful .NET Bot face and the other a simple neutral face." class="wp-image-43754" srcset="https://elbruno.com/wp-content/uploads/2026/10/image-3.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/10/image-3.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/10/image-3.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/10/image-3.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/10/image-3.png?w=1440 1440w, https://elbruno.com/wp-content/uploads/2026/10/image-3.png 1672w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">While building the USB demo for the upcoming <a href="https://aka.ms/newyearnewbot">New Year, New Bot series</a>, I had two small robots connected to one Windows PC. Windows showed several COM ports, but the names did not tell me which cable belonged to which robot.</p>



<p class="wp-block-paragraph">That matters before connecting to a device, and especially before choosing a firmware target. Two different boards can report the same USB vendor and product IDs. A COM number is a current Windows assignment, not a permanent identity.</p>



<h2 class="wp-block-heading">The solution: read metadata, then unplug one cable</h2>



<p class="wp-block-paragraph">Instead of opening each serial port and guessing, this small console app reads Windows Plug and Play metadata: the COM assignment, device description, USB IDs, parent and location. It never opens a port, toggles DTR/RTS, resets a board or flashes firmware.</p>



<p class="wp-block-paragraph">The diagram uses synthetic IDs. Notice that every cable has a clear, continuous path from the PC inventory to its device. The app helps narrow the match; disconnecting one cable and watching for <code>REMOVED</code> confirms which physical device it is.</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="682" src="https://elbruno.com/wp-content/uploads/2026/10/image-4.png?w=1024" alt="Diagram linking three Windows COM inventory entries to two similar robots and a development board with clearly traced USB cables, followed by the unplug-and-label identification steps." class="wp-image-43755" srcset="https://elbruno.com/wp-content/uploads/2026/10/image-4.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/10/image-4.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/10/image-4.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/10/image-4.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/10/image-4.png 1440w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">The code: one file, no project scaffolding</h2>



<p class="wp-block-paragraph">The <a href="https://github.com/elbruno/new-year-new-bot/tree/main/tools/serial-ports">standalone sample</a> targets .NET 10 and uses <code>System.Management</code> to ask Windows for present ports. It reads the COM assignment from the device&#8217;s registry metadata:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">query</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ObjectQuery</span>(</div><div class="cm-line">    <span class="tok-string">&quot;SELECT * FROM Win32_PnPEntity WHERE PNPClass = &apos;Ports&apos; AND Present = TRUE&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">searcher</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ManagementObjectSearcher</span>(</div><div class="cm-line">    <span class="tok-keyword">new</span> <span class="tok-variableName">ManagementScope</span>(<span class="tok-string">@&quot;root\cimv2&quot;</span>),</div><div class="cm-line">    <span class="tok-variableName">query</span>,</div><div class="cm-line">    <span class="tok-keyword">new</span> <span class="tok-variableName">EnumerationOptions</span> { <span class="tok-variableName">Timeout</span> <span class="tok-operator">=</span> <span class="tok-typeName">TimeSpan</span>.<span class="tok-variableName">FromSeconds</span>(<span class="tok-number">5</span>), <span class="tok-variableName">ReturnImmediately</span> <span class="tok-operator">=</span> <span class="tok-atom">false</span> });</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">devices</span> <span class="tok-operator">=</span> <span class="tok-variableName">searcher</span>.<span class="tok-variableName">Get</span>();</div><div class="cm-line"><span class="tok-keyword">foreach</span> (<span class="tok-variableName">ManagementObject</span> <span class="tok-variableName">device</span> <span class="tok-keyword">in</span> <span class="tok-variableName">devices</span>)</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">using</span> (<span class="tok-variableName">device</span>)</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-typeName">string</span><span class="tok-operator">?</span> <span class="tok-variableName">instanceId</span> <span class="tok-operator">=</span> <span class="tok-variableName">device</span>[<span class="tok-string">&quot;PNPDeviceID&quot;</span>] <span class="tok-keyword">as</span> <span class="tok-typeName">string</span>;</div><div class="cm-line">        <span class="tok-keyword">if</span> (<span class="tok-typeName">string</span>.<span class="tok-variableName">IsNullOrWhiteSpace</span>(<span class="tok-variableName">instanceId</span>))</div><div class="cm-line">            <span class="tok-keyword">throw</span> <span class="tok-keyword">new</span> <span class="tok-variableName">InvalidOperationException</span>(<span class="tok-string">&quot;A present port has no PnP instance ID.&quot;</span>);</div><div class="cm-line"></div><div class="cm-line">        <span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">key</span> <span class="tok-operator">=</span> <span class="tok-variableName">Registry</span>.<span class="tok-variableName">LocalMachine</span>.<span class="tok-variableName">OpenSubKey</span>(</div><div class="cm-line">            <span class="tok-variableName">$</span><span class="tok-string">@&quot;SYSTEM\CurrentControlSet\Enum\{instanceId}\Device Parameters&quot;</span>,</div><div class="cm-line">            <span class="tok-variableName">writable</span>: <span class="tok-atom">false</span>);</div><div class="cm-line">        <span class="tok-typeName">string</span><span class="tok-operator">?</span> <span class="tok-variableName">port</span> <span class="tok-operator">=</span> <span class="tok-variableName">key</span><span class="tok-operator">?</span>.<span class="tok-variableName">GetValue</span>(<span class="tok-string">&quot;PortName&quot;</span>) <span class="tok-keyword">as</span> <span class="tok-typeName">string</span>;</div><div class="cm-line">        <span class="tok-comment">// The app validates the COM name, reads optional metadata, and reports it.</span></div><div class="cm-line">    }</div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The full code also handles ports disappearing during a query, orders ports numerically, and reports errors explicitly. <code>--watch</code> compares snapshots and reports added, removed or changed devices. <code>--json</code> makes the snapshot usable by another tool.</p>



<p class="wp-block-paragraph">From the sample folder, with a stable .NET 10 SDK installed:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">run</span> <span class="tok-variableName">SerialPorts</span><span class="tok-punctuation">.</span><span class="tok-variableName">cs</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">run</span> <span class="tok-variableName">SerialPorts</span><span class="tok-punctuation">.</span><span class="tok-variableName">cs</span> <span class="tok-operator">--</span> <span class="tok-operator">--</span><span class="tok-variableName">watch</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">run</span> <span class="tok-variableName">SerialPorts</span><span class="tok-punctuation">.</span><span class="tok-variableName">cs</span> <span class="tok-operator">--</span> <span class="tok-operator">--</span><span class="tok-variableName">json</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">run</span> <span class="tok-variableName">SerialPorts</span><span class="tok-punctuation">.</span><span class="tok-variableName">cs</span> <span class="tok-operator">--</span> <span class="tok-operator">--</span><span class="tok-variableName">self-test</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">For two look-alike devices, run watch mode, unplug just one cable, wait for <code>REMOVED</code>, reconnect it and label that physical robot. Repeat for the other device. Do not use the inventory as an automatic firmware-target selector. The synthetic self-test checks parsing and device changes without WMI, the registry or hardware.</p>



<p class="wp-block-paragraph">This is a small building block for the upcoming <a href="https://aka.ms/newyearnewbot">New Year, New Bot series</a>, where we are exploring a desk companion built with StackChan and .NET.</p>



<p class="wp-block-paragraph"><strong>Image note:</strong> the hero photo was provided by Bruno. The diagram is a hand-authored vector with synthetic port and USB ID values. Other draft illustrations are not used in this shorter version.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://github.com/elbruno/new-year-new-bot/tree/main/tools/serial-ports">Public console sample and instructions</a></li>



<li><a href="https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps">.NET file-based apps</a></li>



<li><a href="https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-pnpentity">Win32_PnPEntity</a></li>
</ul>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/26-12-new-year-new-bot-plan/blob/main/docs/blog/which-device-is-on-my-com-port-dotnet10.md#which-device-is-on-this-com-port-a-tiny-net-10-utility"></a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/10/25/which-device-is-on-this-com-port-a-tiny-net-10-utility/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">43753</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/10/image-3.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/10/image-3.png">
			<media:title type="html">image</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/10/image-3.png?w=1024">
			<media:title type="html">Two StackChan robots beside their retail box. One screen shows a colorful .NET Bot face and the other a simple neutral face.</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/10/image-4.png?w=1024">
			<media:title type="html">Diagram linking three Windows COM inventory entries to two similar robots and a development board with clearly traced USB cables, followed by the unplug-and-label identification steps.</media:title>
		</media:content>
	</item>
		<item>
		<title>From podcast audio to clean subtitles in .NET: Whisper + s1-mini</title>
		<link>https://elbruno.com/2026/10/22/from-podcast-audio-to-clean-subtitles-in-net-whisper-s1-mini/</link>
					<comments>https://elbruno.com/2026/10/22/from-podcast-audio-to-clean-subtitles-in-net-whisper-s1-mini/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Thu, 22 Oct 2026 21:04:28 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[ONNX]]></category>
		<category><![CDATA[Subtitles]]></category>
		<category><![CDATA[webvtt]]></category>
		<category><![CDATA[Whisper]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=43759</guid>

					<description><![CDATA[Hi! After my first post about s1-mini, a reader asked if the library could take the SRT files he generates for his podcast. Then he noticed the catch: the model only rewrites text, it has no idea what a timestamp is. That is fine, because the model does not need to know. The real goal [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="1024" src="https://elbruno.com/wp-content/uploads/2026/10/image-5.png?w=1024" alt="" class="wp-image-43761" srcset="https://elbruno.com/wp-content/uploads/2026/10/image-5.png 1024w, https://elbruno.com/wp-content/uploads/2026/10/image-5.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/10/image-5.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/10/image-5.png?w=768 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">After <a href="https://elbruno.com/2026/08/21/local-transcript-cleanup-in-net-with-s1-mini/" target="_blank" rel="noopener">my first post about s1-mini</a>, a reader asked if the library could take the SRT files he generates for his podcast. Then he noticed the catch: the model only rewrites text, it has no idea what a timestamp is.</p>



<p class="wp-block-paragraph">That is fine, because the model does not need to know. The real goal is bigger than cleaning an existing SRT: <strong>start with an audio recording and end with a subtitle file</strong>. That takes two local models, each doing the one thing it is good at:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Step</th><th>Tool</th><th>Job</th></tr></thead><tbody><tr><td>1</td><td><a href="https://www.nuget.org/packages/ElBruno.Whisper"><code>ElBruno.Whisper</code></a></td><td>Speech to text, <strong>with segment timestamps</strong></td></tr><tr><td>2</td><td><code>ElBruno.S1Mini</code></td><td>Clean each segment&#8217;s text (fillers, stutters, punctuation, numbers)</td></tr><tr><td>3</td><td><code>ElBruno.S1Mini</code></td><td>Write the segments back as SRT or WebVTT, timestamps untouched</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Inference runs on your machine. Model files are downloaded on first use; audio is not uploaded.</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="1024" src="https://elbruno.com/wp-content/uploads/2026/10/image-6.png?w=1024" alt="Flow: subtitle file, parser, text cleanup and writer, with timestamps passing through unchanged" class="wp-image-43762" srcset="https://elbruno.com/wp-content/uploads/2026/10/image-6.png 1024w, https://elbruno.com/wp-content/uploads/2026/10/image-6.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/10/image-6.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/10/image-6.png?w=768 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Why not just give audio to s1-mini?</strong> s1-mini never hears audio and never sees timestamps. Whisper owns the timing. s1-mini only ever receives short pieces of text. The library keeps the two together.</p>
</blockquote>



<h2 class="wp-block-heading">Setup</h2>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-shell"><div class="cm-line">dotnet new console <span class="tok-propertyName">-n</span> PodcastToSrt</div><div class="cm-line"><span class="tok-variableName">cd</span> PodcastToSrt</div><div class="cm-line">dotnet add package ElBruno.S1Mini <span class="tok-propertyName">--version</span> <span class="tok-number">0</span>.2.1</div><div class="cm-line">dotnet add package ElBruno.Whisper <span class="tok-propertyName">--version</span> <span class="tok-number">0</span>.9.0</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The first run downloads the Whisper model and the s1-mini ONNX model, then caches both.</p>



<h2 class="wp-block-heading">Step 1: transcribe the audio with timestamps</h2>



<p class="wp-block-paragraph">Timestamps are opt-in in <code>ElBruno.Whisper</code>. If you forget <code>EnableTimestamps</code>, <code>Segments</code> comes back null:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">Whisper</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">whisper</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">WhisperClient</span>.<span class="tok-variableName">CreateAsync</span>(<span class="tok-keyword">new</span> <span class="tok-variableName">WhisperOptions</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-comment">// Whisper Tiny (English) keeps spoken fillers like &quot;um&quot; and &quot;uh&quot;,</span></div><div class="cm-line">    <span class="tok-comment">// which is exactly what s1-mini is designed to clean up.</span></div><div class="cm-line">    <span class="tok-variableName">Model</span> <span class="tok-operator">=</span> <span class="tok-variableName">KnownWhisperModels</span>.<span class="tok-variableName">All</span>[<span class="tok-number">0</span>],</div><div class="cm-line">    <span class="tok-variableName">EnableTimestamps</span> <span class="tok-operator">=</span> <span class="tok-atom">true</span>,</div><div class="cm-line">});</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">transcription</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">whisper</span>.<span class="tok-variableName">TranscribeAsync</span>(<span class="tok-string">&quot;episode.wav&quot;</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph"><code>transcription.Segments</code> is a list of <code>TranscriptionSegment</code> with <code>Start</code>, <code>End</code> (both <code>TimeSpan</code>) and <code>Text</code>.</p>



<p class="wp-block-paragraph">The snippets below show an <strong>illustrative progression</strong> for the synthetic clip from the real-run example. They explain the shape of each result; Whisper&#8217;s exact transcript and segment boundaries can vary.</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">[00:00:00.000 - 00:00:11.452]</div><div class="cm-line">So I&apos;m welcome back to the the show. Today we are talking about, uh, local transcription in dot net. We will meet on Thursday no wait Friday at ten thirty.</div></code></pre>
		</div>
	</div>
</div>


<h2 class="wp-block-heading">Step 2: turn the segments into subtitle cues</h2>



<p class="wp-block-paragraph"><code>SubtitleCue.FromTimes</code> formats the timing line for you (<code>00:00:01,000</code> for SRT, <code>00:00:01.000</code> for WebVTT):</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">S1Mini</span>.<span class="tok-variableName">Normalization</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">format</span> <span class="tok-operator">=</span> <span class="tok-variableName">SubtitleFormat</span>.<span class="tok-variableName">Srt</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">rawCues</span> <span class="tok-operator">=</span> <span class="tok-variableName">transcription</span>.<span class="tok-variableName">Segments</span><span class="tok-operator">!</span></div><div class="cm-line">    .<span class="tok-variableName">Select</span>(<span class="tok-variableName">s</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">SubtitleCue</span>.<span class="tok-variableName">FromTimes</span>(<span class="tok-variableName">s</span>.<span class="tok-variableName">Start</span>, <span class="tok-variableName">s</span>.<span class="tok-variableName">End</span>, <span class="tok-variableName">s</span>.<span class="tok-variableName">Text</span>.<span class="tok-variableName">Trim</span>(), <span class="tok-variableName">format</span>))</div><div class="cm-line">    .<span class="tok-variableName">ToList</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">rawSrt</span> <span class="tok-operator">=</span> <span class="tok-variableName">SubtitleParser</span>.<span class="tok-variableName">Write</span>(<span class="tok-variableName">rawCues</span>, <span class="tok-variableName">format</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">At this point you already have a valid, timestamped, but still messy subtitle file.</p>



<p class="wp-block-paragraph">For example, the same segment is now an SRT cue. The text is still uncleaned, but its timing is ready to pass through unchanged:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">1</div><div class="cm-line">00:00:00,000 --&gt; 00:00:11,452</div><div class="cm-line">So I&apos;m welcome back to the the show. Today we are talking about, uh, local transcription in dot net. We will meet on Thursday no wait Friday at ten thirty.</div></code></pre>
		</div>
	</div>
</div>


<h2 class="wp-block-heading">Step 3: clean the text, keep the timing</h2>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">normalizer</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">TranscriptNormalizer</span>.<span class="tok-variableName">CreateAsync</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">cleaned</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">normalizer</span>.<span class="tok-variableName">NormalizeSubtitlesAsync</span>(<span class="tok-variableName">rawSrt</span>, <span class="tok-variableName">format</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">await</span> <span class="tok-variableName">File</span>.<span class="tok-variableName">WriteAllTextAsync</span>(<span class="tok-string">&quot;episode.srt&quot;</span>, <span class="tok-variableName">cleaned</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph"><code>NormalizeSubtitlesAsync</code> parses the cues, normalizes each cue&#8217;s text, and writes the file back. Timing lines are carried through as raw strings, so they cannot drift. Markup tags such as <code>&lt;i&gt;</code> are stripped, multi-line cues are joined into one line, and cues that are pure filler stay in place with empty text (or are removed if you pass <code>dropEmptyCues: true</code>, in which case SRT is renumbered).</p>



<p class="wp-block-paragraph">After cleanup, the text is easier to read while the cue timing stays the same:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">1</div><div class="cm-line">00:00:00,000 --&gt; 00:00:11,452</div><div class="cm-line">So I&apos;m welcome back to the show. Today we are talking about a local transcription in .NET. We will meet on Friday at 10:30.</div></code></pre>
		</div>
	</div>
</div>


<h2 class="wp-block-heading">The whole thing in one file</h2>



<p class="wp-block-paragraph">This is the <a href="https://github.com/elbruno/ElBruno.S1Mini/tree/main/src/samples/PodcastToSrt"><code>PodcastToSrt</code></a> sample, trimmed:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">S1Mini</span>.<span class="tok-variableName">Normalization</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">Whisper</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">input</span> <span class="tok-operator">=</span> <span class="tok-variableName">args</span>[<span class="tok-number">0</span>];</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">format</span> <span class="tok-operator">=</span> <span class="tok-variableName">args</span>.<span class="tok-variableName">Contains</span>(<span class="tok-string">&quot;--vtt&quot;</span>) <span class="tok-operator">?</span> <span class="tok-variableName">SubtitleFormat</span>.<span class="tok-variableName">WebVtt</span> : <span class="tok-variableName">SubtitleFormat</span>.<span class="tok-variableName">Srt</span>;</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">output</span> <span class="tok-operator">=</span> <span class="tok-variableName">Path</span>.<span class="tok-variableName">ChangeExtension</span>(<span class="tok-variableName">input</span>, <span class="tok-variableName">format</span> <span class="tok-operator">==</span> <span class="tok-variableName">SubtitleFormat</span>.<span class="tok-variableName">Srt</span> <span class="tok-operator">?</span> <span class="tok-string">&quot;.srt&quot;</span> : <span class="tok-string">&quot;.vtt&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">whisper</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">WhisperClient</span>.<span class="tok-variableName">CreateAsync</span>(<span class="tok-keyword">new</span> <span class="tok-variableName">WhisperOptions</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">Model</span> <span class="tok-operator">=</span> <span class="tok-variableName">KnownWhisperModels</span>.<span class="tok-variableName">All</span>[<span class="tok-number">0</span>],</div><div class="cm-line">    <span class="tok-variableName">EnableTimestamps</span> <span class="tok-operator">=</span> <span class="tok-atom">true</span>,</div><div class="cm-line">});</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">transcription</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">whisper</span>.<span class="tok-variableName">TranscribeAsync</span>(<span class="tok-variableName">input</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">rawCues</span> <span class="tok-operator">=</span> <span class="tok-variableName">transcription</span>.<span class="tok-variableName">Segments</span><span class="tok-operator">!</span></div><div class="cm-line">    .<span class="tok-variableName">Select</span>(<span class="tok-variableName">s</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">SubtitleCue</span>.<span class="tok-variableName">FromTimes</span>(<span class="tok-variableName">s</span>.<span class="tok-variableName">Start</span>, <span class="tok-variableName">s</span>.<span class="tok-variableName">End</span>, <span class="tok-variableName">s</span>.<span class="tok-variableName">Text</span>.<span class="tok-variableName">Trim</span>(), <span class="tok-variableName">format</span>))</div><div class="cm-line">    .<span class="tok-variableName">ToList</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">normalizer</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">TranscriptNormalizer</span>.<span class="tok-variableName">CreateAsync</span>();</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">cleaned</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">normalizer</span>.<span class="tok-variableName">NormalizeSubtitlesAsync</span>(</div><div class="cm-line">    <span class="tok-variableName">SubtitleParser</span>.<span class="tok-variableName">Write</span>(<span class="tok-variableName">rawCues</span>, <span class="tok-variableName">format</span>), <span class="tok-variableName">format</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">await</span> <span class="tok-variableName">File</span>.<span class="tok-variableName">WriteAllTextAsync</span>(<span class="tok-variableName">output</span>, <span class="tok-variableName">cleaned</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Run it:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-shell"><div class="cm-line">dotnet run <span class="tok-propertyName">--</span> episode.wav            <span class="tok-comment"># writes episode.srt</span></div><div class="cm-line">dotnet run <span class="tok-propertyName">--</span> episode.wav <span class="tok-propertyName">--vtt</span>      <span class="tok-comment"># writes episode.vtt</span></div></code></pre>
		</div>
	</div>
</div>


<h2 class="wp-block-heading">A real run</h2>



<p class="wp-block-paragraph">I tested the end-to-end pipeline with a generated WAV file named <code>pod.wav</code> in the system temporary directory. I created it with Windows <code>System.Speech.Synthesis.SpeechSynthesizer</code>, speaking a short script with deliberate fillers, a stutter and a self-correction. It is a synthetic test clip, not a podcast recording:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">&#8220;So um welcome back to the the show. Today we are talking about, uh, local transcription in dot net. We will meet on Thursday no wait Friday at ten thirty.&#8221;</p>
</blockquote>



<p class="wp-block-paragraph">The output file:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">1</div><div class="cm-line">00:00:00,000 --&gt; 00:00:11,452</div><div class="cm-line">So I&apos;m welcome back to the show. Today we are talking about a local transcription in .NET. We will meet on Friday at 10:30.</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The stutter is gone, &#8220;dot net&#8221; became <code>.NET</code>, the self-correction resolved to Friday, and &#8220;ten thirty&#8221; became <code>10:30</code>. Note the <code>So I'm welcome back</code>: that mistake came from Whisper Tiny&#8217;s transcription, and s1-mini cleans text, it does not fix words the speech model got wrong. A bigger Whisper model helps there, at the cost of keeping fewer fillers for s1-mini to clean.</p>



<p class="wp-block-paragraph">This short clip produced a single segment. It did not establish complete-podcast handling or accurate acoustic timing: Whisper 0.9.0 preprocesses at most 30 seconds per call and can return a full-duration fallback segment. The updated repository sample uses explicit windows to process long recordings. The compact one-file example above is only suitable for a short WAV, not a complete podcast.</p>



<h2 class="wp-block-heading">Real Spanish podcasts need a different path</h2>



<p class="wp-block-paragraph">The updated <a href="../../src/samples/PodcastToSrt/README.md"><code>PodcastToSrt</code> sample</a> includes two complete NTN 459 recordings: voice-only <code>Recording.m4a</code> and edited <code>ntn459.mp3</code>, with music. These replace the synthetic clip as the real-audio evaluation fixtures, not as automated unit-test inputs.</p>



<p class="wp-block-paragraph">The original sample selected <code>whisper-tiny.en</code>, an <strong>English-only</strong> model. Spanish needs a multilingual model such as <code>whisper-base</code> or <code>whisper-small</code>, without the <code>.en</code> suffix:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">run</span> <span class="tok-operator">--</span><span class="tok-variableName">project</span> <span class="tok-variableName">src</span><span class="tok-punctuation">\</span><span class="tok-variableName">samples</span><span class="tok-punctuation">\</span><span class="tok-variableName">PodcastToSrt</span> <span class="tok-operator">--</span> <span class="tok-punctuation">`</span></div><div class="cm-line">  <span class="tok-variableName">src</span><span class="tok-punctuation">\</span><span class="tok-variableName">samples</span><span class="tok-punctuation">\</span><span class="tok-variableName">PodcastToSrt</span><span class="tok-punctuation">\</span><span class="tok-variableName">assets</span><span class="tok-punctuation">\</span><span class="tok-variableName">ntn459</span><span class="tok-punctuation">\</span><span class="tok-variableName">Recording</span><span class="tok-punctuation">.</span><span class="tok-variableName">m4a</span> <span class="tok-punctuation">`</span></div><div class="cm-line">  <span class="tok-variableName">artifacts</span><span class="tok-punctuation">\</span><span class="tok-variableName">podcast</span><span class="tok-punctuation">\</span><span class="tok-variableName">voice-es</span><span class="tok-punctuation">.</span><span class="tok-variableName">srt</span> <span class="tok-operator">--</span><span class="tok-variableName">model</span> <span class="tok-variableName">whisper-base</span> <span class="tok-operator">--</span><span class="tok-variableName">language</span> <span class="tok-variableName">es</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">run</span> <span class="tok-operator">--</span><span class="tok-variableName">project</span> <span class="tok-variableName">src</span><span class="tok-punctuation">\</span><span class="tok-variableName">samples</span><span class="tok-punctuation">\</span><span class="tok-variableName">PodcastToSrt</span> <span class="tok-operator">--</span> <span class="tok-punctuation">`</span></div><div class="cm-line">  <span class="tok-variableName">src</span><span class="tok-punctuation">\</span><span class="tok-variableName">samples</span><span class="tok-punctuation">\</span><span class="tok-variableName">PodcastToSrt</span><span class="tok-punctuation">\</span><span class="tok-variableName">assets</span><span class="tok-punctuation">\</span><span class="tok-variableName">ntn459</span><span class="tok-punctuation">\</span><span class="tok-variableName">ntn459</span><span class="tok-punctuation">.</span><span class="tok-variableName">mp3</span> <span class="tok-punctuation">`</span></div><div class="cm-line">  <span class="tok-variableName">artifacts</span><span class="tok-punctuation">\</span><span class="tok-variableName">podcast</span><span class="tok-punctuation">\</span><span class="tok-variableName">edited-es</span><span class="tok-punctuation">.</span><span class="tok-variableName">vtt</span> <span class="tok-operator">--</span><span class="tok-variableName">model</span> <span class="tok-variableName">whisper-small</span> <span class="tok-operator">--</span><span class="tok-variableName">language</span> <span class="tok-variableName">es</span> <span class="tok-operator">--</span><span class="tok-variableName">vtt</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph"><strong>s1-mini v1 is English-only too.</strong> Spanish is preserved without cleanup by default. This separates two questions: how well Whisper recognizes Spanish, and whether normalization preserves that transcription. Changing the cleanup prompt does not make s1-mini a supported Spanish model.</p>



<p class="wp-block-paragraph">The sample also offers <code>--language auto</code>, but the pinned Whisper package does not return reliable detected-language metadata. That mode is explicitly labeled unconfirmed and skips cleanup. In the tested excerpt it generated a repeated “Commission” loop rather than a usable transcript. Prefer an explicit <code>--language es</code> for these recordings.</p>



<p class="wp-block-paragraph">To intentionally produce English subtitles from Spanish audio, add <code>--translate</code>. Whisper translates, then s1-mini cleans the English result. To compare unsupported Spanish cleanup, opt into <code>--cleanup experimental</code>. Both modes retain the pre-cleanup subtitle file for inspection.</p>



<p class="wp-block-paragraph">For compressed and long recordings, the sample uses FFmpeg and non-overlapping windows of at most 25 seconds, retaining original offsets and silence. JSON reports distinguish possible full-window fallback timings from timestamp spans. A tested, dependency-specific byte-decoding adapter also fixes the Spanish accent encoding omitted by Whisper 0.9.0; it does not improve speech recognition.</p>



<p class="wp-block-paragraph">The <a href="../../src/samples/PodcastToSrt/README.md">sample README</a> documents the opt-in comparison commands and limitations. Music, technical names, sentence boundaries and late-episode coverage need review; valid SRT syntax alone is not a quality benchmark.</p>



<p class="wp-block-paragraph">The <a href="../../src/samples/PodcastToSrt/evaluation-results.md">real-audio evaluation</a> processed the complete voice recording into 59 cues and the edited episode into 61 cues, reaching both decoded ends. All cues were coarse full-window spans, so this proves full-file processing, not accurate speech alignment. Spanish technical names still need review, and no human-scored WER/CER is claimed.</p>



<h2 class="wp-block-heading">WebVTT works the same way</h2>



<p class="wp-block-paragraph">Pass <code>SubtitleFormat.WebVtt</code> (or <code>--vtt</code> in the sample). The parser keeps WebVTT cue identifiers and cue settings like <code>align:start</code>, and skips <code>NOTE</code>, <code>STYLE</code> and <code>REGION</code> blocks.</p>



<h2 class="wp-block-heading">Already have an SRT?</h2>



<p class="wp-block-paragraph">Skip steps 1 and 2. Read your file and call <code>NormalizeSubtitlesAsync</code>. The format is auto-detected (content starting with <code>WEBVTT</code> is WebVTT, everything else is SRT):</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">srt</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">File</span>.<span class="tok-variableName">ReadAllTextAsync</span>(<span class="tok-string">&quot;episode.srt&quot;</span>);</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">cleaned</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">normalizer</span>.<span class="tok-variableName">NormalizeSubtitlesAsync</span>(<span class="tok-variableName">srt</span>);</div></code></pre>
		</div>
	</div>
</div>


<h2 class="wp-block-heading">Testing without a model</h2>



<p class="wp-block-paragraph">The automated unit tests do not consume an audio file. They use a fake <code>IChatClient</code> to test subtitle parsing, writing, timestamp formatting and normalization. The manual end-to-end run described above used the generated <code>pod.wav</code> file. The <code>TranscriptNormalizer(IChatClient)</code> constructor makes the subtitle logic easy to test offline with a fake chat client:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">fake</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">FakeChatClient</span>();</div><div class="cm-line"><span class="tok-variableName">fake</span>.<span class="tok-variableName">EnqueueResponse</span>(<span class="tok-string">&quot;So hello there everyone.&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">fake</span>.<span class="tok-variableName">EnqueueResponse</span>(<span class="tok-string">&quot;&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">fake</span>.<span class="tok-variableName">EnqueueResponse</span>(<span class="tok-string">&quot;Welcome back.&quot;</span>);</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">normalizer</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">TranscriptNormalizer</span>(<span class="tok-variableName">fake</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">output</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">normalizer</span>.<span class="tok-variableName">NormalizeSubtitlesAsync</span>(<span class="tok-variableName">srt</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Assert</span>.<span class="tok-variableName">Equal</span>(<span class="tok-number">3</span>, <span class="tok-variableName">fake</span>.<span class="tok-variableName">CallCount</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The parser and writer are plain code, so the timestamp guarantees are checked with exact string comparisons.</p>



<h2 class="wp-block-heading">Known limitations</h2>



<ul class="wp-block-list">
<li><strong>Cues are cleaned one at a time.</strong> The model is stateless, so it never sees neighboring cues. When a segment ends mid-sentence, punctuation and capitalization are less accurate than on full paragraphs. A future version could merge segments into sentences, clean them, and redistribute the text across the original time ranges.</li>



<li><strong>Timing needs validation.</strong> Cleanup preserves numeric timing values, not independently verified alignment. Whisper may return native spans or full-window fallback timing. Removing words does not create word-level alignment; coarse full-window cues are not production-ready subtitle segmentation.</li>



<li><strong>Transcription errors stay.</strong> s1-mini normalizes text. It does not correct words Whisper misheard.</li>
</ul>



<h2 class="wp-block-heading">Links</h2>



<ul class="wp-block-list">
<li>NuGet: <a href="https://www.nuget.org/packages/ElBruno.S1Mini"><code>ElBruno.S1Mini</code></a></li>



<li>GitHub: <a href="https://github.com/elbruno/ElBruno.S1Mini">elbruno/ElBruno.S1Mini</a></li>



<li>Model: <a href="https://huggingface.co/superwhisper/s1-mini"><code>superwhisper/s1-mini</code></a></li>
</ul>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.S1Mini/blob/main/docs/blogs/26-10-21-srt-support.md#from-podcast-audio-to-clean-subtitles-in-net-whisper--s1-mini"></a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/10/22/from-podcast-audio-to-clean-subtitles-in-net-whisper-s1-mini/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">43759</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/10/image-5.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/10/image-5.png">
			<media:title type="html">image</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/10/image-5.png?w=1024"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/10/image-6.png?w=1024">
			<media:title type="html">Flow: subtitle file, parser, text cleanup and writer, with timestamps passing through unchanged</media:title>
		</media:content>
	</item>
		<item>
		<title>Less chat. More decisions: introducing ElBruno.AI.Decisions for .NET</title>
		<link>https://elbruno.com/2026/10/09/less-chat-more-decisions-introducing-elbruno-ai-decisions-for-net/</link>
					<comments>https://elbruno.com/2026/10/09/less-chat-more-decisions-introducing-elbruno-ai-decisions-for-net/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Sat, 10 Oct 2026 01:45:54 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Jev]]></category>
		<category><![CDATA[Microsoft Foundry]]></category>
		<category><![CDATA[Microsoft-Decision-1]]></category>
		<category><![CDATA[NuGet]]></category>
		<category><![CDATA[Ollama]]></category>
		<category><![CDATA[Open Source]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=43747</guid>

					<description><![CDATA[Hi! Sometimes I ask an AI model a very small question: Should this request go to billing or support? And the model replies with a paragraph. Then another paragraph. Then a very polite explanation of why billing departments exist. Nice conversation. Not exactly what my application needed. For classification, routing and evaluation, I often want [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="576" src="https://elbruno.com/wp-content/uploads/2026/10/image-2.png?w=1024" alt="" class="wp-image-43748" srcset="https://elbruno.com/wp-content/uploads/2026/10/image-2.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/10/image-2.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/10/image-2.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/10/image-2.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/10/image-2.png?w=1440 1440w, https://elbruno.com/wp-content/uploads/2026/10/image-2.png 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">Sometimes I ask an AI model a very small question:</p>



<p class="wp-block-paragraph"><strong>Should this request go to billing or support?</strong></p>



<p class="wp-block-paragraph">And the model replies with a paragraph. Then another paragraph. Then a very polite explanation of why billing departments exist.</p>



<p class="wp-block-paragraph">Nice conversation.</p>



<p class="wp-block-paragraph">Not exactly what my application needed.</p>



<p class="wp-block-paragraph">For classification, routing and evaluation, I often want a different shape of answer: a label, a score, or the probability of a proposition. Something I can use from C# without asking a chat model to please, pretty please, return only JSON.</p>



<p class="wp-block-paragraph">That is the idea behind <strong>ElBruno.AI.Decisions</strong>.</p>



<p class="wp-block-paragraph"><strong>Less chat. More decisions.</strong></p>



<p class="wp-block-paragraph">The libraries are on <a href="https://www.nuget.org/packages/ElBruno.AI.Decisions/0.6.2">NuGet</a>, and the code is on <a href="https://github.com/elbruno/ElBruno.AI.Decisions">GitHub</a>.</p>



<h2 class="wp-block-heading">What is a decision model?</h2>



<p class="wp-block-paragraph">A chat model is useful when you need generated text. A decision model is designed for workloads such as classification and evaluation, returning structured decision results.</p>



<p class="wp-block-paragraph">In this SDK, the shared API has three operations:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Operation</th><th>Question</th><th>Result</th></tr></thead><tbody><tr><td><code>ChooseAsync</code></td><td>Which team should handle this ticket?</td><td>One label, the probability distribution, and confidence</td></tr><tr><td><code>ScoreAsync</code></td><td>How complete is this answer?</td><td>A fractional score against ordered rubric levels</td></tr><tr><td><code>AssessAsync</code></td><td>Is this request about a payment problem?</td><td>A proposition probability between 0 and 1</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">No hidden conversion from a probability into a business decision. If your application needs a threshold or a human-review queue, you define it.</p>



<p class="wp-block-paragraph">That last part matters.</p>



<p class="wp-block-paragraph">The model can help choose a route. It should not quietly become your business policy.</p>



<h2 class="wp-block-heading">Four packages, one shared API</h2>



<p class="wp-block-paragraph">The project started around Jev. With Microsoft-Decision-1 available in Foundry and native decision support in Ollama, the original name became too narrow.</p>



<p class="wp-block-paragraph">So the repository is now <strong>ElBruno.AI.Decisions</strong>, with four packages:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Package</th><th>What it does</th></tr></thead><tbody><tr><td><a href="https://www.nuget.org/packages/ElBruno.AI.Decisions/0.6.2">ElBruno.AI.Decisions</a></td><td>Provider-neutral <code>IDecisionClient</code>, result types and chat-routing integration</td></tr><tr><td><a href="https://www.nuget.org/packages/ElBruno.AI.Decisions.Foundry/0.6.2">ElBruno.AI.Decisions.Foundry</a></td><td>Microsoft-Decision-1 through Microsoft&#8217;s SystemOne endpoint</td></tr><tr><td><a href="https://www.nuget.org/packages/ElBruno.AI.Decisions.Ollama/0.6.2">ElBruno.AI.Decisions.Ollama</a></td><td>Native local System One decision models</td></tr><tr><td><a href="https://www.nuget.org/packages/ElBruno.AI.Decisions.Jev/0.6.2">ElBruno.AI.Decisions.Jev</a></td><td>Native TypeSafe AI Jev SDK and a shared-API adapter</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Install a provider package and NuGet brings in the core abstractions automatically.</p>



<p class="wp-block-paragraph">The examples below target <strong>.NET 10</strong> and version <strong>0.6.2</strong>. This is a community SDK, not an official Microsoft, Ollama or TypeSafe AI SDK.</p>



<h2 class="wp-block-heading">Quickstart 1: Microsoft-Decision-1 in Foundry</h2>



<p class="wp-block-paragraph">First, deploy <strong>Microsoft-Decision-1</strong> in your Foundry project.</p>



<p class="wp-block-paragraph">Important detail: this model is from <strong>Microsoft</strong>, not OpenAI. The invocation endpoint is:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">https://&lt;resource&gt;.services.ai.azure.com/providers/microsoft/v1/systemone</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Not <code>/chat/completions</code>. Not <code>/mai/v1/decisions</code>.</p>



<p class="wp-block-paragraph">Use your <strong>deployment name</strong> in the <code>Model</code> setting. That name may be different from the catalog name <code>Microsoft-Decision-1</code>.</p>



<p class="wp-block-paragraph">Create a console app and install the package:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">new</span> <span class="tok-variableName">console</span> <span class="tok-operator">--</span><span class="tok-variableName">framework</span> <span class="tok-variableName">net10</span><span class="tok-number">.0</span> <span class="tok-operator">--</span><span class="tok-variableName">name</span> <span class="tok-variableName">FoundryDecisionsDemo</span></div><div class="cm-line"><span class="tok-variableName">cd</span> <span class="tok-variableName">FoundryDecisionsDemo</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">add</span> <span class="tok-variableName">package</span> <span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">AI</span><span class="tok-punctuation">.</span><span class="tok-variableName">Decisions</span><span class="tok-punctuation">.</span><span class="tok-variableName">Foundry</span> <span class="tok-operator">--</span><span class="tok-variableName">version</span> <span class="tok-number">0.6.2</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">For this quickstart, use API-key authentication through an environment variable. Replace the resource and deployment placeholders in <code>Program.cs</code>:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">AI</span>.<span class="tok-variableName">Decisions</span>.<span class="tok-variableName">Foundry</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">client</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">FoundryDecisionClient</span>(<span class="tok-keyword">new</span> <span class="tok-variableName">FoundryDecisionOptions</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">Endpoint</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">Uri</span>(<span class="tok-string">&quot;https://&lt;resource&gt;.services.ai.azure.com&quot;</span>),</div><div class="cm-line">    <span class="tok-variableName">Model</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;&lt;deployment-name&gt;&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">ApiKey</span> <span class="tok-operator">=</span> <span class="tok-variableName">Environment</span>.<span class="tok-variableName">GetEnvironmentVariable</span>(<span class="tok-string">&quot;FOUNDRY_API_KEY&quot;</span>)</div><div class="cm-line">        <span class="tok-operator">??</span> <span class="tok-keyword">throw</span> <span class="tok-keyword">new</span> <span class="tok-variableName">InvalidOperationException</span>(<span class="tok-string">&quot;Set FOUNDRY_API_KEY.&quot;</span>)</div><div class="cm-line">});</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">teams</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">Dictionary</span><span class="tok-operator">&lt;</span><span class="tok-typeName">string</span>, <span class="tok-typeName">string</span><span class="tok-operator">?&gt;</span></div><div class="cm-line">{</div><div class="cm-line">    [<span class="tok-string">&quot;billing&quot;</span>] <span class="tok-operator">=</span> <span class="tok-string">&quot;Invoices and payments&quot;</span>,</div><div class="cm-line">    [<span class="tok-string">&quot;support&quot;</span>] <span class="tok-operator">=</span> <span class="tok-string">&quot;Technical problems&quot;</span></div><div class="cm-line">};</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">result</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">client</span>.<span class="tok-variableName">ChooseAsync</span>(</div><div class="cm-line">    <span class="tok-string">&quot;Please correct my invoice.&quot;</span>,</div><div class="cm-line">    <span class="tok-string">&quot;Which team should handle this request?&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">teams</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Team: {result.Choice}&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Label probability: {result.Probabilities[result.Choice]:P1}&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Confidence: {result.Confidence:P1}&quot;</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Set <code>FOUNDRY_API_KEY</code> in your shell using your secret-management workflow, then run <code>dotnet run</code>. Do not put the key in source control.</p>



<p class="wp-block-paragraph">Passing the resource root automatically appends the SystemOne path. You can also pass the complete invocation URL.</p>



<p class="wp-block-paragraph">Without an API key, the client uses <code>AzureCliCredential</code> after <code>az login</code>. The live smoke test used API-key authentication; CLI authentication has not been smoke-tested.</p>



<p class="wp-block-paragraph">An <strong>illustrative</strong> provider response looks like this:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-json"><div class="cm-line"><span class="tok-punctuation">{</span></div><div class="cm-line">  <span class="tok-propertyName">&quot;answers&quot;</span><span class="tok-punctuation">:</span> <span class="tok-punctuation">{</span></div><div class="cm-line">    <span class="tok-propertyName">&quot;decision&quot;</span><span class="tok-punctuation">:</span> <span class="tok-punctuation">{</span></div><div class="cm-line">      <span class="tok-propertyName">&quot;type&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;choice&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">      <span class="tok-propertyName">&quot;choice&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;billing&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">      <span class="tok-propertyName">&quot;probabilities&quot;</span><span class="tok-punctuation">:</span> <span class="tok-punctuation">{</span> <span class="tok-propertyName">&quot;billing&quot;</span><span class="tok-punctuation">:</span> <span class="tok-number">0.99</span><span class="tok-punctuation">,</span> <span class="tok-propertyName">&quot;support&quot;</span><span class="tok-punctuation">:</span> <span class="tok-number">0.01</span> <span class="tok-punctuation">}</span><span class="tok-punctuation">,</span></div><div class="cm-line">      <span class="tok-propertyName">&quot;confidence&quot;</span><span class="tok-punctuation">:</span> <span class="tok-number">0.95</span></div><div class="cm-line">    <span class="tok-punctuation">}</span></div><div class="cm-line">  <span class="tok-punctuation">}</span></div><div class="cm-line"><span class="tok-punctuation">}</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Which gives output like:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Team: billing</div><div class="cm-line">Label probability: 99.0%</div><div class="cm-line">Confidence: 95.0%</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Formatting follows your current culture. Values depend on the model and input.</p>



<p class="wp-block-paragraph">Notice that the selected-label probability and confidence are different numbers. Keep them separate.</p>



<p class="wp-block-paragraph">Full runnable sample: <a href="https://github.com/elbruno/ElBruno.AI.Decisions/tree/main/samples/12-FoundryDecision">FoundryDecision</a>.</p>



<h2 class="wp-block-heading">Quickstart 2: local decisions with Ollama</h2>



<p class="wp-block-paragraph">Now the local version.</p>



<p class="wp-block-paragraph">You need <strong>Ollama v0.35.0 or later</strong>, a running local server, and an actual <strong>System One decision model</strong>.</p>



<p class="wp-block-paragraph">Not every model installed in Ollama is a decision model. A chat model does not become one because we ask nicely.</p>



<p class="wp-block-paragraph">For example:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">ollama</span> <span class="tok-variableName">pull</span> <span class="tok-variableName">nimble</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">new</span> <span class="tok-variableName">console</span> <span class="tok-operator">--</span><span class="tok-variableName">framework</span> <span class="tok-variableName">net10</span><span class="tok-number">.0</span> <span class="tok-operator">--</span><span class="tok-variableName">name</span> <span class="tok-variableName">OllamaDecisionsDemo</span></div><div class="cm-line"><span class="tok-variableName">cd</span> <span class="tok-variableName">OllamaDecisionsDemo</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">add</span> <span class="tok-variableName">package</span> <span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">AI</span><span class="tok-punctuation">.</span><span class="tok-variableName">Decisions</span><span class="tok-punctuation">.</span><span class="tok-variableName">Ollama</span> <span class="tok-operator">--</span><span class="tok-variableName">version</span> <span class="tok-number">0.6.2</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">With Ollama running, replace <code>Program.cs</code>:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">AI</span>.<span class="tok-variableName">Decisions</span>.<span class="tok-variableName">Ollama</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">client</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">OllamaDecisionClient</span>(<span class="tok-keyword">new</span> <span class="tok-variableName">OllamaDecisionOptions</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">Endpoint</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">Uri</span>(<span class="tok-string">&quot;http://localhost:11434/&quot;</span>),</div><div class="cm-line">    <span class="tok-variableName">Model</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;nimble&quot;</span></div><div class="cm-line">});</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">teams</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">Dictionary</span><span class="tok-operator">&lt;</span><span class="tok-typeName">string</span>, <span class="tok-typeName">string</span><span class="tok-operator">?&gt;</span></div><div class="cm-line">{</div><div class="cm-line">    [<span class="tok-string">&quot;billing&quot;</span>] <span class="tok-operator">=</span> <span class="tok-string">&quot;Invoices and payments&quot;</span>,</div><div class="cm-line">    [<span class="tok-string">&quot;support&quot;</span>] <span class="tok-operator">=</span> <span class="tok-string">&quot;Technical problems&quot;</span></div><div class="cm-line">};</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">result</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">client</span>.<span class="tok-variableName">ChooseAsync</span>(</div><div class="cm-line">    <span class="tok-string">&quot;Please correct my invoice.&quot;</span>,</div><div class="cm-line">    <span class="tok-string">&quot;Which team should handle this request?&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">teams</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Team: {result.Choice}&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Label probability: {result.Probabilities[result.Choice]:P1}&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Confidence: {result.Confidence:P1}&quot;</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Run <code>dotnet run</code>. No API key is required for the local endpoint.</p>



<p class="wp-block-paragraph">The SDK calls:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">http://localhost:11434/v1/systemone</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">It sends native <code>state</code> and named <code>questions</code>. No chat prompt, no letter mapping, no first-token logprob workaround.</p>



<p class="wp-block-paragraph">Here is an <strong>illustrative response</strong>, not the result of a live local-model test:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-json"><div class="cm-line"><span class="tok-punctuation">{</span></div><div class="cm-line">  <span class="tok-propertyName">&quot;model&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;nimble&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">  <span class="tok-propertyName">&quot;answers&quot;</span><span class="tok-punctuation">:</span> <span class="tok-punctuation">{</span></div><div class="cm-line">    <span class="tok-propertyName">&quot;decision&quot;</span><span class="tok-punctuation">:</span> <span class="tok-punctuation">{</span></div><div class="cm-line">      <span class="tok-propertyName">&quot;type&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;choice&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">      <span class="tok-propertyName">&quot;choice&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;billing&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">      <span class="tok-propertyName">&quot;probabilities&quot;</span><span class="tok-punctuation">:</span> <span class="tok-punctuation">{</span> <span class="tok-propertyName">&quot;billing&quot;</span><span class="tok-punctuation">:</span> <span class="tok-number">0.9</span><span class="tok-punctuation">,</span> <span class="tok-propertyName">&quot;support&quot;</span><span class="tok-punctuation">:</span> <span class="tok-number">0.1</span> <span class="tok-punctuation">}</span><span class="tok-punctuation">,</span></div><div class="cm-line">      <span class="tok-propertyName">&quot;confidence&quot;</span><span class="tok-punctuation">:</span> <span class="tok-number">0.531</span></div><div class="cm-line">    <span class="tok-punctuation">}</span></div><div class="cm-line">  <span class="tok-punctuation">}</span><span class="tok-punctuation">,</span></div><div class="cm-line">  <span class="tok-propertyName">&quot;usage&quot;</span><span class="tok-punctuation">:</span> <span class="tok-punctuation">{</span> <span class="tok-propertyName">&quot;input_tokens&quot;</span><span class="tok-punctuation">:</span> <span class="tok-number">174</span><span class="tok-punctuation">,</span> <span class="tok-propertyName">&quot;output_tokens&quot;</span><span class="tok-punctuation">:</span> <span class="tok-number">1</span> <span class="tok-punctuation">}</span></div><div class="cm-line"><span class="tok-punctuation">}</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Example output:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Team: billing</div><div class="cm-line">Label probability: 90.0%</div><div class="cm-line">Confidence: 53.1%</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Ollama confidence measures distribution concentration:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">1 - H(p) / ln(N)</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">It is <strong>not calibrated correctness</strong> and it is <strong>not the probability of the selected label</strong>.</p>



<p class="wp-block-paragraph">Full runnable sample: <a href="https://github.com/elbruno/ElBruno.AI.Decisions/tree/main/samples/11-OllamaLocal">OllamaLocal</a>.</p>



<h2 class="wp-block-heading">Same client, different questions</h2>



<p class="wp-block-paragraph">The Foundry and Ollama clients both implement <code>IDecisionClient</code>. With either <code>client</code> from the examples above, scoring and assessment use the same methods:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">score</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">client</span>.<span class="tok-variableName">ScoreAsync</span>(</div><div class="cm-line">    <span class="tok-string">&quot;Restart the app, check the logs, and include the error message.&quot;</span>,</div><div class="cm-line">    <span class="tok-string">&quot;How actionable is this support answer?&quot;</span>,</div><div class="cm-line">    <span class="tok-keyword">new</span>[]</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-string">&quot;No useful action&quot;</span>,</div><div class="cm-line">        <span class="tok-string">&quot;Some guidance, but important details are missing&quot;</span>,</div><div class="cm-line">        <span class="tok-string">&quot;Clear steps with useful diagnostic information&quot;</span></div><div class="cm-line">    });</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Expected rubric level: {score.Score:F2}&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">assessment</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">client</span>.<span class="tok-variableName">AssessAsync</span>(</div><div class="cm-line">    <span class="tok-string">&quot;My card was charged twice for the same order.&quot;</span>,</div><div class="cm-line">    <span class="tok-string">&quot;This request concerns a payment problem.&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Payment problem probability: {assessment.Probability:P1}&quot;</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Score uses an <strong>ordered rubric</strong>, lowest level first. With these three levels, the result is a fractional, zero-based position between 0 and 2. It is not automatically normalized to 0-1, and it is not rounded to an integer.</p>



<p class="wp-block-paragraph">Assess returns a proposition probability. If the application needs a cutoff, make that policy explicit and validate it on representative data.</p>



<h2 class="wp-block-heading">Quickstart 3: native Jev questions</h2>



<p class="wp-block-paragraph">The Jev package also exposes its native typed API, useful when you need structured state, named questions and provider-specific metadata.</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">new</span> <span class="tok-variableName">console</span> <span class="tok-operator">--</span><span class="tok-variableName">framework</span> <span class="tok-variableName">net10</span><span class="tok-number">.0</span> <span class="tok-operator">--</span><span class="tok-variableName">name</span> <span class="tok-variableName">JevDecisionsDemo</span></div><div class="cm-line"><span class="tok-variableName">cd</span> <span class="tok-variableName">JevDecisionsDemo</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">add</span> <span class="tok-variableName">package</span> <span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">AI</span><span class="tok-punctuation">.</span><span class="tok-variableName">Decisions</span><span class="tok-punctuation">.</span><span class="tok-variableName">Jev</span> <span class="tok-operator">--</span><span class="tok-variableName">version</span> <span class="tok-number">0.6.2</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Set <code>JEV_API_KEY</code> through your secret-management workflow and replace <code>Program.cs</code>:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">AI</span>.<span class="tok-variableName">Decisions</span>.<span class="tok-variableName">Jev</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">client</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">JevClient</span>(<span class="tok-keyword">new</span> <span class="tok-variableName">JevClientOptions</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">ApiKey</span> <span class="tok-operator">=</span> <span class="tok-variableName">Environment</span>.<span class="tok-variableName">GetEnvironmentVariable</span>(<span class="tok-string">&quot;JEV_API_KEY&quot;</span>)</div><div class="cm-line">        <span class="tok-operator">??</span> <span class="tok-keyword">throw</span> <span class="tok-keyword">new</span> <span class="tok-variableName">InvalidOperationException</span>(<span class="tok-string">&quot;Set JEV_API_KEY.&quot;</span>),</div><div class="cm-line">    <span class="tok-variableName">DefaultModel</span> <span class="tok-operator">=</span> <span class="tok-variableName">JevModels</span>.<span class="tok-variableName">Version1_13_0</span></div><div class="cm-line">});</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">question</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">JevQuestionKey</span><span class="tok-operator">&lt;</span><span class="tok-variableName">JevNoulAnswer</span><span class="tok-operator">&gt;</span>(<span class="tok-string">&quot;relevant&quot;</span>);</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">request</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">JevDecisionRequest</span>(</div><div class="cm-line">    <span class="tok-string">&quot;A .NET developer asks about dependency injection.&quot;</span>)</div><div class="cm-line">    .<span class="tok-variableName">WithQuestion</span>(</div><div class="cm-line">        <span class="tok-variableName">question</span>,</div><div class="cm-line">        <span class="tok-keyword">new</span> <span class="tok-variableName">JevNoulQuestion</span>(<span class="tok-string">&quot;Is this about .NET development?&quot;</span>));</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">response</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">client</span>.<span class="tok-variableName">EvaluateAsync</span>(<span class="tok-variableName">request</span>);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Relevance: {response.GetAnswer(question).Probability:P1}&quot;</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Run <code>dotnet run</code> with a valid service credential and access to the configured model. This quickstart demonstrates the SDK contract; live Jev compatibility remains unverified.</p>



<p class="wp-block-paragraph">For shared-API routing and the rest of the Jev examples, start with the <a href="https://github.com/elbruno/ElBruno.AI.Decisions/tree/main/samples">repository samples</a>.</p>



<h2 class="wp-block-heading">Where would I use this?</h2>



<p class="wp-block-paragraph">A few practical starting points:</p>



<ul class="wp-block-list">
<li>Route a support request to billing or technical support.</li>



<li>Classify GitHub issues into a fixed set of labels.</li>



<li>Score an answer against an explicit completeness rubric.</li>



<li>Assess whether a retrieved document is relevant to a question.</li>



<li>Choose which tool or workflow should handle a request.</li>
</ul>



<p class="wp-block-paragraph">The useful pattern is not &#8220;replace every chat model.&#8221;</p>



<p class="wp-block-paragraph">It is <strong>use the right output shape for the task</strong>.</p>



<p class="wp-block-paragraph">If you need a paragraph, use a chat model. If you need a decision distribution, start with a decision model.</p>



<h2 class="wp-block-heading">Important: early access is still early access</h2>



<p class="wp-block-paragraph">Version 0.6.2 is published on NuGet, with a separate README for each package. That is not a production-readiness certificate.</p>



<p class="wp-block-paragraph">Here is the current status:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Area</th><th>Status</th></tr></thead><tbody><tr><td>Foundry Choice, Score and Assess</td><td>Live API-key smoke test completed on October 9, 2026</td></tr><tr><td>Foundry CLI authentication</td><td>Implemented, not live smoke-tested</td></tr><tr><td>Local Ollama decision-model compatibility</td><td>Unverified; covered by offline protocol checks and synthetic samples</td></tr><tr><td>Live Jev compatibility</td><td>Unverified</td></tr><tr><td>Shared Foundry/Ollama surface</td><td>One text question per call; no image or batch API exposed</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The shared Score API supports 2-10 rubric levels. Ollama text requests are bounded at 64 KiB. Model-specific choice limits still apply.</p>



<p class="wp-block-paragraph">Confidence is not a safety guarantee. A clean probability distribution is not proof that the model understood your business.</p>



<p class="wp-block-paragraph">Measure on your own data. Keep a human-review path where appropriate. And do not turn an illustrative example into a benchmark slide.</p>



<h2 class="wp-block-heading">Try the samples without credentials</h2>



<p class="wp-block-paragraph">The repository samples include an explicit offline mode:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">git</span> <span class="tok-variableName">clone</span> <span class="tok-variableName">https</span><span class="tok-operator">://</span><span class="tok-variableName">github</span><span class="tok-punctuation">.</span><span class="tok-variableName">com</span><span class="tok-operator">/</span><span class="tok-variableName">elbruno</span><span class="tok-operator">/</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">AI</span><span class="tok-punctuation">.</span><span class="tok-variableName">Decisions</span><span class="tok-punctuation">.</span><span class="tok-variableName">git</span></div><div class="cm-line"><span class="tok-variableName">cd</span> <span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">AI</span><span class="tok-punctuation">.</span><span class="tok-variableName">Decisions</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">run</span> <span class="tok-operator">--</span><span class="tok-variableName">project</span> <span class="tok-variableName">samples</span><span class="tok-punctuation">\</span><span class="tok-number">12</span><span class="tok-operator">-</span><span class="tok-variableName">FoundryDecision</span> <span class="tok-operator">--</span> <span class="tok-operator">--</span><span class="tok-variableName">offline</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">run</span> <span class="tok-operator">--</span><span class="tok-variableName">project</span> <span class="tok-variableName">samples</span><span class="tok-punctuation">\</span><span class="tok-number">11</span><span class="tok-operator">-</span><span class="tok-variableName">OllamaLocal</span> <span class="tok-operator">--</span> <span class="tok-operator">--</span><span class="tok-variableName">offline</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">These use synthetic responses. Great for exploring the API, not for measuring real model quality.</p>



<p class="wp-block-paragraph">For real deployments, use the <a href="https://github.com/elbruno/ElBruno.AI.Decisions/blob/main/docs/configuration.md">configuration and user-secrets guide</a>.</p>



<h2 class="wp-block-heading">Links</h2>



<ul class="wp-block-list">
<li><a href="https://github.com/elbruno/ElBruno.AI.Decisions">Repository, source and samples</a></li>



<li><a href="https://www.nuget.org/packages/ElBruno.AI.Decisions/0.6.2">NuGet core package</a></li>



<li><a href="https://www.nuget.org/packages/ElBruno.AI.Decisions.Foundry/0.6.2">Foundry package</a></li>



<li><a href="https://www.nuget.org/packages/ElBruno.AI.Decisions.Ollama/0.6.2">Ollama package</a></li>



<li><a href="https://www.nuget.org/packages/ElBruno.AI.Decisions.Jev/0.6.2">Jev package</a></li>



<li><a href="https://commandline.microsoft.com/microsoft-decision-1-model-foundry/">Microsoft-Decision-1 introduction</a></li>



<li><a href="https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/introducing-microsoft-decision-1-in-microsoft-foundry-for-decision-and-classific/4562742">Microsoft&#8217;s Foundry endpoint example</a></li>



<li><a href="https://docs.ollama.com/capabilities/decision">Ollama decision guide</a></li>



<li><a href="https://docs.ollama.com/api/systemone">Ollama System One API</a></li>



<li><a href="https://docs.typesafe.ai/">TypeSafe AI documentation</a></li>
</ul>



<h2 class="wp-block-heading">Wrap-up</h2>



<p class="wp-block-paragraph">I like this direction because it makes a common application need explicit: sometimes we need AI to decide, not to talk.</p>



<p class="wp-block-paragraph">Foundry when I want a hosted Microsoft model. Ollama when I want a local decision model. Jev when I want its native typed workflow.</p>



<p class="wp-block-paragraph">One shared API where it makes sense, provider-specific features where they matter.</p>



<p class="wp-block-paragraph">Less chat. More decisions. And hopefully, fewer paragraphs explaining why my invoice belongs to billing.</p>



<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">El Bruno</p>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/10/09/less-chat-more-decisions-introducing-elbruno-ai-decisions-for-net/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">43747</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/10/image-2.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/10/image-2.png">
			<media:title type="html">image</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/10/image-2.png?w=1024"/>
	</item>
		<item>
		<title>From a ZIP Archive to an AI Ingestion Document with C# and MarkItDotNet</title>
		<link>https://elbruno.com/2026/10/09/from-a-zip-archive-to-an-ai-ingestion-document-with-c-and-markitdotnet/</link>
					<comments>https://elbruno.com/2026/10/09/from-a-zip-archive-to-an-ai-ingestion-document-with-c-and-markitdotnet/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Fri, 09 Oct 2026 23:01:15 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[Data Ingestion]]></category>
		<category><![CDATA[Document Processing]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Generative AI]]></category>
		<category><![CDATA[MarkDown]]></category>
		<category><![CDATA[MarkItDotNet]]></category>
		<category><![CDATA[NuGet]]></category>
		<category><![CDATA[Open Source]]></category>
		<category><![CDATA[rag]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=43742</guid>

					<description><![CDATA[Draft for future publication and promotion. The DataIngestion adapter is available as 0.9.3-preview.1. The Markdown and ZIP examples use the current repository source, without assuming those features are already available in a stable release. Hi! Converting a PDF to Markdown is great. But when you are preparing documents for an AI pipeline, the next question [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="576" src="https://elbruno.com/wp-content/uploads/2026/10/image.png?w=1024" alt="" class="wp-image-43744" srcset="https://elbruno.com/wp-content/uploads/2026/10/image.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/10/image.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/10/image.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/10/image.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/10/image.png?w=1440 1440w, https://elbruno.com/wp-content/uploads/2026/10/image.png 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Draft for future publication and promotion. The DataIngestion adapter is available as <code>0.9.3-preview.1</code>. The Markdown and ZIP examples use the current repository source, without assuming those features are already available in a stable release.</p>
</blockquote>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">Converting a PDF to Markdown is great. But when you are preparing documents for an AI pipeline, the next question is: <strong>what do I do with that Markdown afterward?</strong></p>



<p class="wp-block-paragraph">A document is more than a string. It has titles, sections, tables, lists, code examples, and references to its source. Flatten everything too early, and you lose information that could help you decide how to split it, enrich it, or display a citation.</p>



<p class="wp-block-paragraph">I added three pieces to <a href="https://github.com/elbruno/ElBruno.MarkItDotNet">ElBruno.MarkItDotNet</a> to support that next step: structured Markdown import with Markdig, a ZIP connector that does not extract files to disk, and an optional adapter for <code>Microsoft.Extensions.DataIngestion</code>.</p>



<p class="wp-block-paragraph">The goal is not to implement an entire AI pipeline in one library. It is to connect its stages well.</p>



<h2 class="wp-block-heading">First: What Is Available and How to Try It</h2>



<p class="wp-block-paragraph">The <code>ElBruno.MarkItDotNet.DataIngestion</code> adapter targets .NET 8 and .NET 10. Its Microsoft dependency is in preview and stays isolated from CoreModel.</p>



<p class="wp-block-paragraph">To try <strong>the published adapter</strong> in a new application:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">new</span> <span class="tok-variableName">console</span> <span class="tok-operator">-</span><span class="tok-variableName">n</span> <span class="tok-variableName">AdapterDemo</span> <span class="tok-operator">-f</span> <span class="tok-variableName">net10</span><span class="tok-number">.0</span></div><div class="cm-line"><span class="tok-variableName">Set-Location</span> <span class="tok-variableName">AdapterDemo</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">add</span> <span class="tok-variableName">package</span> <span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">MarkItDotNet</span><span class="tok-punctuation">.</span><span class="tok-variableName">DataIngestion</span> <span class="tok-operator">--</span><span class="tok-variableName">version</span> <span class="tok-number">0.9.3</span><span class="tok-operator">-</span><span class="tok-variableName">preview</span><span class="tok-number">.1</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">This command uses the .NET 10 SDK. The adapter also includes a .NET 8 target. CoreModel comes in as a dependency of the adapter. You do not need to install Microsoft&#8217;s full pipeline package to call <code>ToIngestionDocument()</code>.</p>



<p class="wp-block-paragraph">To try <strong>all three pieces together</strong>, the repository includes a runnable demo that uses project references:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">git</span> <span class="tok-variableName">clone</span> <span class="tok-variableName">https</span><span class="tok-operator">://</span><span class="tok-variableName">github</span><span class="tok-punctuation">.</span><span class="tok-variableName">com</span><span class="tok-operator">/</span><span class="tok-variableName">elbruno</span><span class="tok-operator">/</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">MarkItDotNet</span><span class="tok-punctuation">.</span><span class="tok-variableName">git</span></div><div class="cm-line"><span class="tok-variableName">Set-Location</span> <span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">MarkItDotNet</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">run</span> <span class="tok-operator">--</span><span class="tok-variableName">project</span> <span class="tok-variableName">src</span><span class="tok-punctuation">\</span><span class="tok-variableName">samples</span><span class="tok-punctuation">\</span><span class="tok-variableName">MarkdownIngestionDemo</span><span class="tok-punctuation">\</span><span class="tok-variableName">MarkdownIngestionDemo</span><span class="tok-punctuation">.</span><span class="tok-variableName">csproj</span> <span class="tok-operator">--</span><span class="tok-variableName">framework</span> <span class="tok-variableName">net8</span><span class="tok-number">.0</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The demo creates its own test ZIP containing Markdown, JSON, and text, processes the entries, and deletes the temporary archive when it finishes. It does not need AI services, API keys, or private documents.</p>



<h2 class="wp-block-heading">1. A Minimal Adapter Example Using Only NuGet</h2>



<p class="wp-block-paragraph">Let&#8217;s start small: build a document and adapt it.</p>



<p class="wp-block-paragraph">This complete program can replace <code>Program.cs</code> in <code>AdapterDemo</code>:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">MarkItDotNet</span>.<span class="tok-variableName">CoreModel</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">MarkItDotNet</span>.<span class="tok-variableName">DataIngestion</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">model</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">Document</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">Id</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;release-notes&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">Metadata</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">DocumentMetadata</span></div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">Title</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;Release notes&quot;</span>,</div><div class="cm-line">        <span class="tok-variableName">SourceFormat</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;generated&quot;</span></div><div class="cm-line">    },</div><div class="cm-line">    <span class="tok-variableName">Sections</span> <span class="tok-operator">=</span></div><div class="cm-line">    [</div><div class="cm-line">        <span class="tok-keyword">new</span> <span class="tok-variableName">DocumentSection</span></div><div class="cm-line">        {</div><div class="cm-line">            <span class="tok-variableName">Id</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;changes&quot;</span>,</div><div class="cm-line">            <span class="tok-variableName">Heading</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">HeadingBlock</span></div><div class="cm-line">            {</div><div class="cm-line">                <span class="tok-variableName">Id</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;changes-heading&quot;</span>,</div><div class="cm-line">                <span class="tok-variableName">Text</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;Changes&quot;</span>,</div><div class="cm-line">                <span class="tok-variableName">Level</span> <span class="tok-operator">=</span> <span class="tok-number">2</span></div><div class="cm-line">            },</div><div class="cm-line">            <span class="tok-variableName">Blocks</span> <span class="tok-operator">=</span></div><div class="cm-line">            [</div><div class="cm-line">                <span class="tok-keyword">new</span> <span class="tok-variableName">ParagraphBlock</span></div><div class="cm-line">                {</div><div class="cm-line">                    <span class="tok-variableName">Id</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;intro&quot;</span>,</div><div class="cm-line">                    <span class="tok-variableName">Text</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;Documents now have structure.&quot;</span></div><div class="cm-line">                }</div><div class="cm-line">            ]</div><div class="cm-line">        }</div><div class="cm-line">    ]</div><div class="cm-line">};</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">ingestion</span> <span class="tok-operator">=</span> <span class="tok-variableName">model</span>.<span class="tok-variableName">ToIngestionDocument</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Identifier: {ingestion.Identifier}&quot;</span>);</div><div class="cm-line"><span class="tok-keyword">foreach</span> (<span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">element</span> <span class="tok-keyword">in</span> <span class="tok-variableName">ingestion</span>.<span class="tok-variableName">EnumerateContent</span>())</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;{element.GetType().Name}: {element.GetMarkdown()}&quot;</span>);</div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The result keeps the document identifier. The title becomes a level 1 header, while the section heading and paragraph become native Microsoft ingestion elements.</p>



<p class="wp-block-paragraph">There is no hidden LLM call here. No embeddings or automatic indexing. This is a change of representation so that other components can consume the document.</p>



<h2 class="wp-block-heading">2. Markdown Becomes More Than a String</h2>



<p class="wp-block-paragraph">In the repository demo, this Markdown is our input:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-markdown"><div class="cm-line"><span class="tok-heading tok-meta">#</span><span class="tok-heading"> Deployment guide</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-heading tok-meta">##</span><span class="tok-heading"> Configuration</span></div><div class="cm-line"></div><div class="cm-line">Use <span class="tok-strong tok-meta">**</span><span class="tok-strong">environment variables</span><span class="tok-strong tok-meta">**</span> for configuration.</div><div class="cm-line"></div><div class="cm-line">| Setting | Value |</div><div class="cm-line">| --- | --- |</div><div class="cm-line">| Mode | Preview |</div><div class="cm-line"></div><div class="cm-line"><span class="tok-heading tok-meta">##</span><span class="tok-heading"> Checklist</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-meta">-</span> <span class="tok-link tok-meta">[</span><span class="tok-link">x</span><span class="tok-link tok-meta">]</span> Build the application</div><div class="cm-line"><span class="tok-meta">-</span> [ ] Review the deployment</div><div class="cm-line"></div><div class="cm-line"><span class="tok-meta">```</span><span class="tok-labelName">csharp</span></div><div class="cm-line">Console.WriteLine(&quot;Ready!&quot;);</div><div class="cm-line"><span class="tok-meta">```</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-meta">&gt;</span> Review before deploying.</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The new import API is intentionally small:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">MarkItDotNet</span>.<span class="tok-variableName">Markdown</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">model</span> <span class="tok-operator">=</span> <span class="tok-variableName">MarkdownDocumentParser</span>.<span class="tok-variableName">Parse</span>(<span class="tok-variableName">markdown</span>, <span class="tok-string">&quot;docs/deployment.md&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Title: {model.Metadata.Title}&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Format: {model.Metadata.SourceFormat}&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Document ID: {model.Id}&quot;</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Markdig parses CommonMark and its advanced extensions. The parser maps the result to <code>Document</code>, <code>DocumentSection</code>, and CoreModel blocks.</p>



<p class="wp-block-paragraph">An initial H1 becomes the document title. Other headings form the section hierarchy. Paragraphs, tables, lists, code, and block quotes have their own representations; unsupported block content is retained as raw Markdown.</p>



<p class="wp-block-paragraph">The existing Markdown converter keeps its behavior: <strong>structured import is optional</strong>, not a mandatory replacement for Markdown pass-through.</p>



<h3 class="wp-block-heading">Walk Sections Without Losing Their Hierarchy</h3>



<p class="wp-block-paragraph">Reading only <code>model.Sections</code> is not enough: sections can contain subsections. This helper walks both:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">MarkItDotNet</span>.<span class="tok-variableName">CoreModel</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">static</span> <span class="tok-keyword">void</span> <span class="tok-variableName">PrintSections</span>(</div><div class="cm-line">    <span class="tok-variableName">IReadOnlyList</span><span class="tok-operator">&lt;</span><span class="tok-variableName">DocumentSection</span><span class="tok-operator">&gt;</span> <span class="tok-variableName">sections</span>,</div><div class="cm-line">    <span class="tok-typeName">int</span> <span class="tok-variableName">depth</span> <span class="tok-operator">=</span> <span class="tok-number">0</span>)</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">foreach</span> (<span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">section</span> <span class="tok-keyword">in</span> <span class="tok-variableName">sections</span>)</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">indent</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-typeName">string</span>(<span class="tok-string">&apos; &apos;</span>, <span class="tok-variableName">depth</span> <span class="tok-operator">*</span> <span class="tok-number">2</span>);</div><div class="cm-line">        <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;{indent}Section: {section.Heading?.Text ?? &quot;</span>(<span class="tok-variableName">root</span>)<span class="tok-string">&quot;}&quot;</span>);</div><div class="cm-line"></div><div class="cm-line">        <span class="tok-keyword">foreach</span> (<span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">block</span> <span class="tok-keyword">in</span> <span class="tok-variableName">section</span>.<span class="tok-variableName">Blocks</span>)</div><div class="cm-line">        {</div><div class="cm-line">            <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;{indent}  {block.GetType().Name}, ID: {block.Id}&quot;</span>);</div><div class="cm-line">        }</div><div class="cm-line"></div><div class="cm-line">        <span class="tok-variableName">PrintSections</span>(<span class="tok-variableName">section</span>.<span class="tok-variableName">SubSections</span>, <span class="tok-variableName">depth</span> <span class="tok-operator">+</span> <span class="tok-number">1</span>);</div><div class="cm-line">    }</div><div class="cm-line">}</div><div class="cm-line"></div><div class="cm-line"><span class="tok-comment">// Inside your program:</span></div><div class="cm-line"><span class="tok-comment">// PrintSections(model.Sections);</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The example input produces <code>ParagraphBlock</code>, <code>TableBlock</code>, <code>ListBlock</code>, <code>CodeBlock</code>, and <code>QuoteBlock</code> instances.</p>



<h3 class="wp-block-heading">Inspect a Checklist</h3>



<p class="wp-block-paragraph">In our example document, <code>Checklist</code> is a top-level section:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">checklist</span> <span class="tok-operator">=</span> <span class="tok-variableName">model</span>.<span class="tok-variableName">Sections</span>.<span class="tok-typeName">Single</span>(</div><div class="cm-line">    <span class="tok-variableName">section</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">section</span>.<span class="tok-variableName">Heading</span><span class="tok-operator">?</span>.<span class="tok-variableName">Text</span> <span class="tok-operator">==</span> <span class="tok-string">&quot;Checklist&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">tasks</span> <span class="tok-operator">=</span> <span class="tok-variableName">checklist</span>.<span class="tok-variableName">Blocks</span>.<span class="tok-variableName">OfType</span><span class="tok-operator">&lt;</span><span class="tok-variableName">ListBlock</span><span class="tok-operator">&gt;</span>().<span class="tok-typeName">Single</span>();</div><div class="cm-line"><span class="tok-keyword">foreach</span> (<span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">item</span> <span class="tok-keyword">in</span> <span class="tok-variableName">tasks</span>.<span class="tok-variableName">Items</span>)</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;{item.Text}: checked={item.IsChecked}&quot;</span>);</div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph"><code>IsChecked</code> is nullable: <code>true</code> and <code>false</code> represent task items; <code>null</code> identifies an ordinary list item.</p>



<h2 class="wp-block-heading">3. Save the Structure and Render Markdown Again</h2>



<p class="wp-block-paragraph">Serializing the model to JSON is a useful way to inspect its full structure:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">System</span>.<span class="tok-variableName">Text</span>.<span class="tok-variableName">Json</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">json</span> <span class="tok-operator">=</span> <span class="tok-variableName">JsonSerializer</span>.<span class="tok-variableName">Serialize</span>(</div><div class="cm-line">    <span class="tok-variableName">model</span>,</div><div class="cm-line">    <span class="tok-keyword">new</span> <span class="tok-variableName">JsonSerializerOptions</span> { <span class="tok-variableName">WriteIndented</span> <span class="tok-operator">=</span> <span class="tok-atom">true</span> });</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">json</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Blocks use polymorphic discriminators such as <code>$type</code>. The JSON shows which content is a paragraph, a table, or a code block, along with its IDs and source references.</p>



<p class="wp-block-paragraph">We can also render Markdown again:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">MarkItDotNet</span>.<span class="tok-variableName">CoreModel</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">rendered</span> <span class="tok-operator">=</span> <span class="tok-variableName">MarkdownRenderer</span>.<span class="tok-variableName">Render</span>(<span class="tok-variableName">model</span>);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">rendered</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">There is an important distinction: <code>OriginalMarkdown</code> retains the syntax of the <strong>block</strong>, including bold text, links, or code fences. The renderer prioritizes that value when it is present.</p>



<p class="wp-block-paragraph">This is not a promise of byte-for-byte reproduction of the entire file: spacing between blocks, line endings, and the title may be normalized. Editing <code>Text</code> also does not automatically invalidate <code>OriginalMarkdown</code>.</p>



<p class="wp-block-paragraph">If I want to replace a parsed paragraph, I make that decision explicit:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">paragraph</span> <span class="tok-operator">=</span> <span class="tok-variableName">model</span>.<span class="tok-variableName">Sections</span></div><div class="cm-line">    .<span class="tok-variableName">SelectMany</span>(<span class="tok-variableName">section</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">section</span>.<span class="tok-variableName">Blocks</span>)</div><div class="cm-line">    .<span class="tok-variableName">OfType</span><span class="tok-operator">&lt;</span><span class="tok-variableName">ParagraphBlock</span><span class="tok-operator">&gt;</span>()</div><div class="cm-line">    .<span class="tok-variableName">First</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">edited</span> <span class="tok-operator">=</span> <span class="tok-variableName">paragraph</span> <span class="tok-variableName">with</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">Text</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;Use managed configuration for production.&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">OriginalMarkdown</span> <span class="tok-operator">=</span> <span class="tok-atom">null</span></div><div class="cm-line">};</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">edited</span>.<span class="tok-variableName">Text</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph"><code>with</code> creates a new block. To render the updated document, you must include that block in a new section or document. The original block does not change.</p>



<h2 class="wp-block-heading">4. ZIP as a Document Source, Not a Temporary Folder</h2>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="576" src="https://elbruno.com/wp-content/uploads/2026/10/image-1.png?w=1024" alt="" class="wp-image-43745" srcset="https://elbruno.com/wp-content/uploads/2026/10/image-1.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/10/image-1.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/10/image-1.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/10/image-1.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/10/image-1.png?w=1440 1440w, https://elbruno.com/wp-content/uploads/2026/10/image-1.png 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">A ZIP often arrives as a documentation bundle: guides, notes, JSON files, or reports. The connector exposes its files as <code>SourceDocument</code> instances through <code>IDocumentSource</code>.</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">MarkItDotNet</span>.<span class="tok-variableName">Connectors</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">Microsoft</span>.<span class="tok-variableName">Extensions</span>.<span class="tok-variableName">Logging</span>.<span class="tok-variableName">Abstractions</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">source</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ZipArchiveConnector</span>(</div><div class="cm-line">    <span class="tok-keyword">new</span> <span class="tok-variableName">ZipArchiveConnectorOptions</span></div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">ArchivePath</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;documents.zip&quot;</span>,</div><div class="cm-line">        <span class="tok-variableName">MaxArchiveSizeBytes</span> <span class="tok-operator">=</span> <span class="tok-number">10</span> <span class="tok-operator">*</span> <span class="tok-number">1024</span> <span class="tok-operator">*</span> <span class="tok-number">1024</span>,</div><div class="cm-line">        <span class="tok-variableName">MaxEntrySizeBytes</span> <span class="tok-operator">=</span> <span class="tok-number">2</span> <span class="tok-operator">*</span> <span class="tok-number">1024</span> <span class="tok-operator">*</span> <span class="tok-number">1024</span>,</div><div class="cm-line">        <span class="tok-variableName">MaxTotalUncompressedBytes</span> <span class="tok-operator">=</span> <span class="tok-number">20</span> <span class="tok-operator">*</span> <span class="tok-number">1024</span> <span class="tok-operator">*</span> <span class="tok-number">1024</span>,</div><div class="cm-line">        <span class="tok-variableName">MaxEntries</span> <span class="tok-operator">=</span> <span class="tok-number">100</span></div><div class="cm-line">    },</div><div class="cm-line">    <span class="tok-variableName">NullLogger</span><span class="tok-operator">&lt;</span><span class="tok-variableName">ZipArchiveConnector</span><span class="tok-operator">&gt;</span>.<span class="tok-variableName">Instance</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">timeout</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">CancellationTokenSource</span>(<span class="tok-typeName">TimeSpan</span>.<span class="tok-variableName">FromSeconds</span>(<span class="tok-number">30</span>));</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">await</span> <span class="tok-keyword">foreach</span> (<span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">document</span> <span class="tok-keyword">in</span> <span class="tok-variableName">source</span>.<span class="tok-variableName">GetDocumentsAsync</span>(<span class="tok-variableName">timeout</span>.<span class="tok-variableName">Token</span>))</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">document</span>.<span class="tok-variableName">Name</span>);</div><div class="cm-line">    <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">document</span>.<span class="tok-variableName">Source</span>);</div><div class="cm-line">    <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">document</span>.<span class="tok-variableName">Metadata</span>[<span class="tok-variableName">SourceMetadataKeys</span>.<span class="tok-variableName">ArchiveEntryPath</span>]);</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">await</span> <span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">content</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">document</span>.<span class="tok-variableName">OpenReadAsync</span>(<span class="tok-variableName">timeout</span>.<span class="tok-variableName">Token</span>);</div><div class="cm-line">    <span class="tok-comment">// Consume the stream here, before leaving this iteration.</span></div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Enumeration discovers the files. <code>OpenReadAsync()</code> opens their content on demand. The returned stream keeps the ZIP archive open until the stream is disposed.</p>



<p class="wp-block-paragraph">Directory entries are not returned as documents. Entry names serve as references and <strong>are not turned into extraction paths</strong>.</p>



<p class="wp-block-paragraph">For applications that use dependency injection:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">MarkItDotNet</span>.<span class="tok-variableName">Connectors</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">Microsoft</span>.<span class="tok-variableName">Extensions</span>.<span class="tok-variableName">DependencyInjection</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">services</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ServiceCollection</span>();</div><div class="cm-line"><span class="tok-variableName">services</span>.<span class="tok-variableName">AddZipArchiveConnector</span>(<span class="tok-variableName">options</span> <span class="tok-operator">=&gt;</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">options</span>.<span class="tok-variableName">ArchivePath</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;documents.zip&quot;</span>;</div><div class="cm-line">    <span class="tok-variableName">options</span>.<span class="tok-variableName">MaxEntries</span> <span class="tok-operator">=</span> <span class="tok-number">100</span>;</div><div class="cm-line">    <span class="tok-variableName">options</span>.<span class="tok-variableName">MaxEntrySizeBytes</span> <span class="tok-operator">=</span> <span class="tok-number">2</span> <span class="tok-operator">*</span> <span class="tok-number">1024</span> <span class="tok-operator">*</span> <span class="tok-number">1024</span>;</div><div class="cm-line">});</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">provider</span> <span class="tok-operator">=</span> <span class="tok-variableName">services</span>.<span class="tok-variableName">BuildServiceProvider</span>();</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">source</span> <span class="tok-operator">=</span> <span class="tok-variableName">provider</span>.<span class="tok-variableName">GetRequiredService</span><span class="tok-operator">&lt;</span><span class="tok-variableName">IDocumentSource</span><span class="tok-operator">&gt;</span>();</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">This DI example also requires <code>Microsoft.Extensions.DependencyInjection</code>, the concrete container. The connectors package provides registration extensions and abstractions, not that container.</p>



<h3 class="wp-block-heading">Limits Matter</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Option</th><th>Default</th><th>What It Controls</th></tr></thead><tbody><tr><td><code>MaxArchiveSizeBytes</code></td><td>100 MiB</td><td>Compressed archive size during discovery</td></tr><tr><td><code>MaxEntrySizeBytes</code></td><td>25 MiB</td><td>Declared entry size and the cap applied during reads</td></tr><tr><td><code>MaxTotalUncompressedBytes</code></td><td>250 MiB</td><td>Sum of declared sizes of eligible entries</td></tr><tr><td><code>MaxEntries</code></td><td>10,000</td><td>Entry count, including directories</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Entries exceeding the per-entry limit are skipped with a warning. If the archive size, entry count, or eligible total exceeds its limit, the connector rejects the operation.</p>



<p class="wp-block-paragraph">Reads have an additional per-entry cap. The total limit is not a shared counter across concurrent or repeated reads; it is a discovery-time check based on declared sizes.</p>



<p class="wp-block-paragraph">This should not be presented as universal protection against malicious ZIP archives. There is no recursive extraction of nested ZIPs, decompression CPU budget, or configurable compression-ratio limit. In production, I would still use concurrency limits, isolation, and application-specific policies for untrusted files.</p>



<h2 class="wp-block-heading">5. ZIP, Conversion, Parsing, and Adaptation: The Complete Flow</h2>



<p class="wp-block-paragraph">The four stages can be connected without writing intermediate files:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">MarkItDotNet</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">MarkItDotNet</span>.<span class="tok-variableName">DataIngestion</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">MarkItDotNet</span>.<span class="tok-variableName">Markdown</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">converter</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">MarkdownConverter</span>();</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">allowedExtensions</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">HashSet</span><span class="tok-operator">&lt;</span><span class="tok-typeName">string</span><span class="tok-operator">&gt;</span>(<span class="tok-variableName">StringComparer</span>.<span class="tok-variableName">OrdinalIgnoreCase</span>)</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-string">&quot;.md&quot;</span>, <span class="tok-string">&quot;.txt&quot;</span>, <span class="tok-string">&quot;.json&quot;</span></div><div class="cm-line">};</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">await</span> <span class="tok-keyword">foreach</span> (<span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">document</span> <span class="tok-keyword">in</span> <span class="tok-variableName">source</span>.<span class="tok-variableName">GetDocumentsAsync</span>(<span class="tok-variableName">timeout</span>.<span class="tok-variableName">Token</span>))</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">extension</span> <span class="tok-operator">=</span> <span class="tok-variableName">Path</span>.<span class="tok-variableName">GetExtension</span>(<span class="tok-variableName">document</span>.<span class="tok-variableName">Name</span>);</div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-operator">!</span><span class="tok-variableName">allowedExtensions</span>.<span class="tok-variableName">Contains</span>(<span class="tok-variableName">extension</span>))</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Skipped unsupported document: {document.Name}&quot;</span>);</div><div class="cm-line">        <span class="tok-keyword">continue</span>;</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">await</span> <span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">content</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">document</span>.<span class="tok-variableName">OpenReadAsync</span>(<span class="tok-variableName">timeout</span>.<span class="tok-variableName">Token</span>);</div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">convertedMarkdown</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">converter</span>.<span class="tok-variableName">ConvertAsync</span>(</div><div class="cm-line">        <span class="tok-variableName">content</span>, <span class="tok-variableName">extension</span>, <span class="tok-variableName">timeout</span>.<span class="tok-variableName">Token</span>);</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">structured</span> <span class="tok-operator">=</span> <span class="tok-variableName">MarkdownDocumentParser</span>.<span class="tok-variableName">Parse</span>(</div><div class="cm-line">        <span class="tok-variableName">convertedMarkdown</span>, <span class="tok-variableName">document</span>.<span class="tok-variableName">Source</span>);</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">ingestionDocument</span> <span class="tok-operator">=</span> <span class="tok-variableName">structured</span>.<span class="tok-variableName">ToIngestionDocument</span>();</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(</div><div class="cm-line">        <span class="tok-variableName">$</span><span class="tok-string">&quot;{document.Name}: {ingestionDocument.EnumerateContent().Count()} elements&quot;</span>);</div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">This snippet reuses <code>source</code> and <code>timeout</code> from the earlier example. The complete program is in the demo.</p>



<p class="wp-block-paragraph">The allowlist is an application decision. We can extend it to formats supported by registered converters, but a ZIP does not magically make every file inside it compatible.</p>



<p class="wp-block-paragraph">There is also a traceability detail: <code>sourcePath</code> includes the ZIP archive and entry, but the parser&#8217;s offsets refer to the <strong>converted Markdown</strong>, not equivalent positions in the original PDF or DOCX.</p>



<p class="wp-block-paragraph">One more distinction: no intermediate files does not mean constant memory usage. The converter produces a string and the parser builds a tree; the ZIP connector avoids extracting every entry to disk.</p>



<h2 class="wp-block-heading">6. Native Tables, Not Just Text with Pipes</h2>



<p class="wp-block-paragraph">The adapter maps a table to <code>IngestionDocumentTable</code> with a cell matrix:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">Microsoft</span>.<span class="tok-variableName">Extensions</span>.<span class="tok-variableName">DataIngestion</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">ingestion</span> <span class="tok-operator">=</span> <span class="tok-variableName">model</span>.<span class="tok-variableName">ToIngestionDocument</span>();</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">table</span> <span class="tok-operator">=</span> <span class="tok-variableName">ingestion</span>.<span class="tok-variableName">EnumerateContent</span>()</div><div class="cm-line">    .<span class="tok-variableName">OfType</span><span class="tok-operator">&lt;</span><span class="tok-variableName">IngestionDocumentTable</span><span class="tok-operator">&gt;</span>()</div><div class="cm-line">    .<span class="tok-typeName">Single</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Rows: {table.Cells.GetLength(0)}&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Columns: {table.Cells.GetLength(1)}&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Header: {table.Cells[0, 0]?.Text}&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Value: {table.Cells[1, 1]?.Text}&quot;</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">For our input, the first row contains <code>Setting</code> and <code>Value</code>; the second contains <code>Mode</code> and <code>Preview</code>.</p>



<p class="wp-block-paragraph">This makes the structure available to downstream components that need to handle tables differently from paragraphs.</p>



<h2 class="wp-block-heading">7. IDs and Source References Travel Too</h2>



<p class="wp-block-paragraph">The adapter&#8217;s metadata uses the <code>markitdotnet.</code> prefix:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">root</span> <span class="tok-operator">=</span> <span class="tok-variableName">ingestion</span>.<span class="tok-variableName">Sections</span>.<span class="tok-typeName">Single</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">root</span>.<span class="tok-variableName">Metadata</span>[<span class="tok-string">&quot;markitdotnet.documentId&quot;</span>]);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">root</span>.<span class="tok-variableName">Metadata</span>[<span class="tok-string">&quot;markitdotnet.sourceFormat&quot;</span>]);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">foreach</span> (<span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">element</span> <span class="tok-keyword">in</span> <span class="tok-variableName">ingestion</span>.<span class="tok-variableName">EnumerateContent</span>())</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-variableName">element</span>.<span class="tok-variableName">Metadata</span>.<span class="tok-variableName">TryGetValue</span>(<span class="tok-string">&quot;markitdotnet.blockId&quot;</span>, <span class="tok-keyword">out</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">blockId</span>))</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Block: {blockId}&quot;</span>);</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-variableName">element</span>.<span class="tok-variableName">Metadata</span>.<span class="tok-variableName">TryGetValue</span>(<span class="tok-string">&quot;markitdotnet.sourceOffset&quot;</span>, <span class="tok-keyword">out</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">offset</span>))</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Markdown offset: {offset}&quot;</span>);</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-variableName">element</span>.<span class="tok-variableName">Metadata</span>.<span class="tok-variableName">TryGetValue</span>(<span class="tok-string">&quot;markitdotnet.sourcePath&quot;</span>, <span class="tok-keyword">out</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">path</span>))</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Source: {path}&quot;</span>);</div><div class="cm-line">    }</div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Document metadata is stored on the root section. Block references are stored on the corresponding element; <code>PageNumber</code> is also copied when present.</p>



<p class="wp-block-paragraph">The parser generates a deterministic ID for the same content and source path. That does not mean the ID stays the same after editing the file, moving it, or changing the converted Markdown.</p>



<p class="wp-block-paragraph">Lists, block quotes, and code blocks do not all have dedicated native equivalents in this adapter: they are represented as paragraphs with Markdown and block-type metadata. The internal hierarchy of a list or quote is not mapped to a hierarchy of Microsoft ingestion elements.</p>



<h2 class="wp-block-heading">Why an Optional Package?</h2>



<p class="wp-block-paragraph">Because not every consumer needs the same thing.</p>



<p class="wp-block-paragraph">If I want to convert a file to Markdown, I keep using the main package. If I want to inspect Markdown structure, I use the optional parser. If I want to integrate CoreModel with Microsoft components, I add DataIngestion.</p>



<p class="wp-block-paragraph">The adapter references <code>Microsoft.Extensions.DataIngestion.Abstractions</code>. It does not parse Markdown again with Microsoft&#8217;s reader or force the base library to depend on a preview API.</p>



<p class="wp-block-paragraph">That gives us a clear boundary of responsibility: <strong>MarkItDotNet prepares and represents documents; your application decides how to use them</strong>.</p>



<h2 class="wp-block-heading">Try It and Follow the Project</h2>



<p class="wp-block-paragraph">The complete example for this article is in <a href="../src/samples/MarkdownIngestionDemo/Program.cs">MarkdownIngestionDemo</a>. You can run it on .NET 8 or .NET 10 by changing <code>--framework</code>.</p>



<p class="wp-block-paragraph">The <a href="markdown-import.md">Markdown import guide</a>, <a href="zip-connector.md">ZIP connector guide</a>, and <a href="data-ingestion.md">adapter guide</a> cover each API separately.</p>



<p class="wp-block-paragraph">Find the source and packages here:</p>



<ul class="wp-block-list">
<li><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">ElBruno.MarkItDotNet repository</a>.</li>



<li><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.DataIngestion/0.9.3-preview.1">DataIngestion adapter 0.9.3-preview.1 on NuGet</a>.</li>



<li><a href="https://learn.microsoft.com/dotnet/dataingestion/microsoft-extensions-dataingestion">Microsoft.Extensions.DataIngestion</a>.</li>
</ul>



<p class="wp-block-paragraph">If you are preparing documents for AI with C#, give the demo a try. Sometimes the most useful next step is not another model call: it is preserving more of the structure your documents already have.</p>



<p class="wp-block-paragraph">Cheers!</p>



<p class="wp-block-paragraph">Bruno</p>



<h2 class="wp-block-heading">Materials for Future Promotion</h2>



<p class="wp-block-paragraph"><strong>Short title:</strong> Markdown + ZIP + DataIngestion: Structured Documents with C#.</p>



<p class="wp-block-paragraph"><strong>Excerpt:</strong> ElBruno.MarkItDotNet adds structured Markdown import, ZIP reading without extraction, and an optional adapter for Microsoft.Extensions.DataIngestion. A runnable C# demo connects all three pieces.</p>



<p class="wp-block-paragraph"><strong>Social post:</strong> A document is more than a string. With ElBruno.MarkItDotNet, you can preserve sections, tables, and source references, read ZIP entries without extracting them to disk, and adapt CoreModel to IngestionDocument. The adapter is in preview, and the repo includes a complete demo. Code: <a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></p>



<p class="wp-block-paragraph"><strong>Images:</strong> Original hero and pipeline diagram in PNG, with editable SVG sources in <code>images/</code>. When publishing outside GitHub, upload both images and replace this draft&#8217;s relative paths with the blog&#8217;s image URLs.</p>



<p class="wp-block-paragraph"><strong>Editorial note:</strong> This article was prepared with AI assistance; examples were checked against the repository APIs. Verify the published Markdown and Connectors versions before replacing the source-based setup with NuGet installation instructions.</p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.MarkItDotNet/blob/main/docs/blog-markdown-zip-data-ingestion.md#from-a-zip-archive-to-an-ai-ingestion-document-with-c-and-markitdotnet"></a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/10/09/from-a-zip-archive-to-an-ai-ingestion-document-with-c-and-markitdotnet/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">43742</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/10/image.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/10/image.png">
			<media:title type="html">image</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/10/image.png?w=1024"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/10/image-1.png?w=1024"/>
	</item>
		<item>
		<title>EmbeddingGemma 2 Integration</title>
		<link>https://elbruno.com/2026/10/08/embeddinggemma-2-integration/</link>
					<comments>https://elbruno.com/2026/10/08/embeddinggemma-2-integration/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Thu, 08 Oct 2026 18:02:02 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[Embeddings]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[GitHub]]></category>
		<category><![CDATA[NuGet]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=43739</guid>

					<description><![CDATA[Hi! EmbeddingGemma 2 Integration ElBruno.LocalEmbeddings.EmbeddingGemma provides local text-only embeddings from Google&#8217;s EmbeddingGemma 2 model through IEmbeddingGenerator&#60;string, Embedding&#60;float&#62;&#62;. The base model is multimodal, but this integration only handles strings. Image, audio, and video inputs need separate preprocessors and are not supported by this package. Install and use For a runnable semantic-search example that compares a query [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<h1 class="wp-block-heading">EmbeddingGemma 2 Integration</h1>



<p class="wp-block-paragraph"><code>ElBruno.LocalEmbeddings.EmbeddingGemma</code> provides local <strong>text-only</strong> embeddings from Google&#8217;s EmbeddingGemma 2 model through <code>IEmbeddingGenerator&lt;string, Embedding&lt;float&gt;&gt;</code>.</p>



<p class="wp-block-paragraph">The base model is multimodal, but this integration only handles strings. Image, audio, and video inputs need separate preprocessors and are not supported by this package.</p>



<h2 class="wp-block-heading">Install and use</h2>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-shell"><div class="cm-line">dotnet add package ElBruno.LocalEmbeddings.EmbeddingGemma</div></code></pre>
		</div>
	</div>
</div>

<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">LocalEmbeddings</span>.<span class="tok-variableName">EmbeddingGemma</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">LocalEmbeddings</span>.<span class="tok-variableName">EmbeddingGemma</span>.<span class="tok-variableName">Options</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">await</span> <span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">generator</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">EmbeddingGemmaEmbeddingGenerator</span>.<span class="tok-variableName">CreateAsync</span>(</div><div class="cm-line">    <span class="tok-keyword">new</span> <span class="tok-variableName">EmbeddingGemmaOptions</span></div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">OutputDimensions</span> <span class="tok-operator">=</span> <span class="tok-number">768</span>,</div><div class="cm-line">        <span class="tok-variableName">MaxSequenceLength</span> <span class="tok-operator">=</span> <span class="tok-number">8192</span></div><div class="cm-line">    });</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">query</span> <span class="tok-operator">=</span> <span class="tok-variableName">EmbeddingGemmaPrompts</span>.<span class="tok-variableName">CreateSearchQuery</span>(<span class="tok-string">&quot;Which planet is known as the Red Planet?&quot;</span>);</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">passage</span> <span class="tok-operator">=</span> <span class="tok-variableName">EmbeddingGemmaPrompts</span>.<span class="tok-variableName">CreateDocument</span>(</div><div class="cm-line">    <span class="tok-string">&quot;Mars is known for its reddish appearance.&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">title</span>: <span class="tok-string">&quot;Mars&quot;</span>);</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">vectors</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">generator</span>.<span class="tok-variableName">GenerateAsync</span>([<span class="tok-variableName">query</span>, <span class="tok-variableName">passage</span>]);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">For a runnable semantic-search example that compares a query against several documents, run:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-shell"><div class="cm-line">dotnet run <span class="tok-propertyName">--project</span> src/Samples/EmbeddingGemmaConsoleApp</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The sample requests 256-dimensional vectors and ranks its documents by cosine similarity. See <a href="../src/Samples/EmbeddingGemmaConsoleApp/README.md">EmbeddingGemmaConsoleApp</a> for prerequisites and customization.</p>



<p class="wp-block-paragraph">Task prefixes are not added automatically. The helper methods use Google&#8217;s recommended retrieval formats:</p>



<ul class="wp-block-list">
<li>Query: <code>task: search result | query: {query}</code></li>



<li>Document: <code>title: {title or none} | text: {content}</code></li>
</ul>



<p class="wp-block-paragraph">For symmetric tasks, use the same task prefix for every input (for example, <code>task: sentence similarity | query: {text}</code>). Avoid comparing vectors created for incompatible tasks or different output dimensions.</p>



<h2 class="wp-block-heading">Configuration</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Option</th><th>Default</th><th>Description</th></tr></thead><tbody><tr><td><code>ModelPath</code></td><td><code>null</code></td><td>Local model directory; it must contain <code>tokenizer.model</code> and <code>onnx/model_q4.onnx</code> with <code>onnx/model_q4.onnx_data</code>.</td></tr><tr><td><code>CacheDirectory</code></td><td>Platform application-data cache</td><td>Parent directory for downloaded model files.</td></tr><tr><td><code>MaxSequenceLength</code></td><td><code>8192</code></td><td>Maximum tokens including BOS/EOS; valid range is 3–8192.</td></tr><tr><td><code>OutputDimensions</code></td><td><code>768</code></td><td>Matryoshka vector size: 128, 256, 512, or 768. Truncated outputs are L2-normalized.</td></tr><tr><td><code>EnsureModelDownloaded</code></td><td><code>true</code></td><td>Download files if missing. Set false to require a valid existing cache or <code>ModelPath</code>.</td></tr><tr><td><code>UseParallelExecution</code></td><td><code>true</code></td><td>ONNX Runtime execution mode.</td></tr><tr><td><code>InterOpNumThreads</code></td><td><code>null</code></td><td>Optional ONNX Runtime inter-op thread count.</td></tr><tr><td><code>IntraOpNumThreads</code></td><td><code>null</code></td><td>Optional ONNX Runtime intra-op thread count.</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Dependency injection is available from <code>ElBruno.LocalEmbeddings.EmbeddingGemma.Extensions</code>:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-variableName">services</span>.<span class="tok-variableName">AddEmbeddingGemmaEmbeddings</span>(<span class="tok-variableName">options</span> <span class="tok-operator">=&gt;</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">options</span>.<span class="tok-variableName">OutputDimensions</span> <span class="tok-operator">=</span> <span class="tok-number">256</span>;</div><div class="cm-line">    <span class="tok-variableName">options</span>.<span class="tok-variableName">CacheDirectory</span> <span class="tok-operator">=</span> <span class="tok-string">@&quot;D:\model-cache&quot;</span>;</div><div class="cm-line">});</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The DI factory initializes the model synchronously on first resolution. In async-first applications, create the generator with <code>CreateAsync</code> and register the instance as a singleton instead.</p>



<h2 class="wp-block-heading">Model files, revision, and licensing</h2>



<p class="wp-block-paragraph">The package downloads three files:</p>



<ul class="wp-block-list">
<li><code>onnx-community/embeddinggemma-2-ONNX</code>, revision <code>daa72c51243991dfcaf9f9137d2c573d8f7790c0</code>: Q4 text graph and its external weight data.</li>



<li><code>google/embeddinggemma-2</code>, revision <code>914f7f89142e33e77833254d9c9b90c3cef7303b</code>: SentencePiece <code>tokenizer.model</code>.</li>
</ul>



<p class="wp-block-paragraph">Each file is checked against its pinned SHA-256 digest before it is used. The Q4 ONNX external weights are approximately 166 MiB (174 MB), the graph is below 1 MiB, and the tokenizer model is approximately 4.7 MB. The export includes Microsoft ONNX Runtime custom quantization operators and is tested with ONNX Runtime 1.24.4.</p>



<p class="wp-block-paragraph">EmbeddingGemma 2 is published under Google&#8217;s Apache 2.0 Gemma 4 license terms. The exporter repository indicates Apache 2.0 as well. Review the source model&#8217;s <a href="https://ai.google.dev/gemma/docs/gemma_4_license">license and terms</a> for your distribution and use case.</p>



<h2 class="wp-block-heading">Offline and local model use</h2>



<p class="wp-block-paragraph">After a successful download, set <code>EnsureModelDownloaded = false</code> to avoid network access:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">await</span> <span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">generator</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">EmbeddingGemmaEmbeddingGenerator</span>.<span class="tok-variableName">CreateAsync</span>(</div><div class="cm-line">    <span class="tok-keyword">new</span> <span class="tok-variableName">EmbeddingGemmaOptions</span> { <span class="tok-variableName">EnsureModelDownloaded</span> <span class="tok-operator">=</span> <span class="tok-atom">false</span> });</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Alternatively, point <code>ModelPath</code> at a directory containing the pinned model layout. Local paths are loaded directly; they are not downloaded or checksum-verified automatically.</p>



<h2 class="wp-block-heading">Output dimensions and context</h2>



<p class="wp-block-paragraph">Google supports 768-dimensional output and Matryoshka truncation to 512, 256, or 128 dimensions. This integration takes the leading dimensions and normalizes the truncated vector. Use the same dimension for every vector in an index.</p>



<p class="wp-block-paragraph">Input length is capped at 8,192 tokens, including BOS and EOS. Longer inputs are truncated by the tokenizer. Chunk longer documents before embedding when complete-document retrieval matters.</p>



<p class="wp-block-paragraph">The model-backed integration test is opt-in because it requires the large ONNX artifacts. With .NET 8 selected, set <code>EMBEDDINGGEMMA_DOWNLOAD_INTEGRATION=1</code> before running the EmbeddingGemma test project to download and verify the pinned model, then execute an end-to-end embedding check. Without this variable, tests use a model already present in <code>EMBEDDINGGEMMA_MODEL_PATH</code> or the default cache and skip the integration check when none is available.</p>



<h2 class="wp-block-heading">Troubleshooting</h2>



<ul class="wp-block-list">
<li><strong>Model download/hash error:</strong> Confirm access to Hugging Face and retry; downloaded files are pinned and verified, so a mismatched artifact is rejected.</li>



<li><strong>Missing external data:</strong> Keep <code>model_q4.onnx_data</code> beside the ONNX graph under the <code>onnx</code> directory.</li>



<li><strong>Local path error:</strong> Verify <code>tokenizer.model</code>, <code>onnx/model_q4.onnx</code>, and <code>onnx/model_q4.onnx_data</code> exist.</li>



<li><strong>Memory pressure or slow startup:</strong> The Q4 graph and external weights total about 166 MiB; including the tokenizer, the first download is about 171 MiB. Expect higher temporary memory requirements while ONNX Runtime loads the graph. Reduce batch size and sequence length for constrained machines.</li>



<li><strong>Different search quality:</strong> Use matching query/document task formats and keep index/query dimensions the same.</li>
</ul>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://ai.google.dev/gemma/docs/embeddinggemma">Google EmbeddingGemma 2 documentation</a></li>



<li><a href="https://huggingface.co/google/embeddinggemma-2">Google model repository</a></li>



<li><a href="https://huggingface.co/onnx-community/embeddinggemma-2-ONNX/tree/daa72c51243991dfcaf9f9137d2c573d8f7790c0">Pinned ONNX export</a></li>



<li><a href="https://huggingface.co/google/embeddinggemma-2/blob/914f7f89142e33e77833254d9c9b90c3cef7303b/tokenizer.model">Hugging Face SentencePiece tokenizer API</a></li>
</ul>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/10/08/embeddinggemma-2-integration/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">43739</post-id>
		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>
	</item>
		<item>
		<title>⚡I Stopped Asking an LLM “Which Team Owns This Ticket?” Now I Get an Answer in 600ms, Locally</title>
		<link>https://elbruno.com/2026/10/01/%e2%9a%a1i-stopped-asking-an-llm-which-team-owns-this-ticket-now-i-get-an-answer-in-600ms-locally/</link>
					<comments>https://elbruno.com/2026/10/01/%e2%9a%a1i-stopped-asking-an-llm-which-team-owns-this-ticket-now-i-get-an-answer-in-600ms-locally/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Fri, 02 Oct 2026 01:57:18 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[calibration]]></category>
		<category><![CDATA[classification]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[decision-model]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[huggingface]]></category>
		<category><![CDATA[laya]]></category>
		<category><![CDATA[ONNX]]></category>
		<category><![CDATA[ONNXRuntime]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=43734</guid>

					<description><![CDATA[Hi! TL;DR I added ElBruno.LocalLLMs.Decisions to the library (shipped in v0.22.0). It runs the Laya decision model family locally, in-process, on ONNX Runtime. No Python. No server. No API key. 🟢 Typed answers, not prose: choice, score, and yes/no — each with a full probability distribution. One forward pass. Asking 4 questions costs about what [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/images/blog-local-decisions.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/images/blog-local-decisions.png" alt="Local decisions — System One fast intuition versus System Two deliberate reasoning, with a laptop running C# in the middle" /></a></figure>



<p class="wp-block-paragraph">Hi!</p>



<h2 class="wp-block-heading">TL;DR</h2>



<p class="wp-block-paragraph">I added <strong><code>ElBruno.LocalLLMs.Decisions</code></strong> to the library (shipped in <strong>v0.22.0</strong>). It runs the <strong><a href="https://github.com/NandhaKishorM/laya">Laya</a></strong> decision model family <strong>locally, in-process, on ONNX Runtime</strong>. No Python. No server. No API key. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f7e2.png" alt="🟢" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<ul class="wp-block-list">
<li><strong>Typed answers</strong>, not prose: <code>choice</code>, <code>score</code>, and yes/no — each with a <strong>full probability distribution</strong>.</li>



<li><strong>One forward pass.</strong> Asking 4 questions costs about what asking 1 costs. Measured: <strong>~590 ms for 4 questions.</strong></li>



<li><strong>Non-autoregressive.</strong> It doesn&#8217;t <em>generate</em> an answer token by token. It reads your text once and reads the probabilities straight off the output.</li>



<li><strong>Honest warning up front:</strong> the public checkpoints are <strong>not reliably calibrated</strong>. The ranking is good. The confidence <em>number</em> needs your own validation. I&#8217;ll show you exactly where it breaks. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f534.png" alt="🔴" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>
</ul>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-shell"><div class="cm-line">dotnet add package ElBruno.LocalLLMs.Decisions</div></code></pre>
		</div>
	</div>
</div>


<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The problem: I was using a sledgehammer to answer a yes/no question</h2>



<p class="wp-block-paragraph">Here&#8217;s the thing I kept doing, and I bet you do it too.</p>



<p class="wp-block-paragraph">I have a support ticket. I want to know which team should get it. So I call a chat model:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">&#8220;You are a helpful classifier. Given the following ticket, respond with exactly one of: billing, technical, sales. Respond with only the word, no explanation. Ticket: &#8230;&#8221;</p>
</blockquote>



<p class="wp-block-paragraph">And then I pray. Because what comes back might be <code>billing</code>. Or <code>Billing</code>. Or <code>"billing"</code>. Or <code>Based on the content, this appears to be a billing issue.</code> Or, my personal favourite, a beautifully formatted markdown table explaining its reasoning when I explicitly asked for one word. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">So I write a parser. Then I write a retry. Then I write a fallback. And all of that is wrapped around a model that is <strong>generating text token by token</strong> to tell me one of three things I already knew the shape of.</p>



<p class="wp-block-paragraph">That&#8217;s <strong>System Two</strong> thinking — slow, deliberate, sequential — used for a <strong>System One</strong> job.</p>



<p class="wp-block-paragraph">System One is the fast one. It&#8217;s the &#8220;that&#8217;s a snake&#8221; reflex before your brain says the word <em>snake</em>. Recognition, not reasoning. And when your job is &#8220;put this in one of three buckets,&#8221; recognition is all you need.</p>



<p class="wp-block-paragraph">That&#8217;s exactly what Laya is built for.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">What &#8220;one forward pass&#8221; actually means</h2>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/assets/local-decisions/one-forward-pass.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/local-decisions/one-forward-pass.png" alt="A single arrow passing through a neural network in one sweep, emerging as probability bars, contrasted with a looping generation spiral" /></a></figure>



<p class="wp-block-paragraph">A normal LLM answering your question does this:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">read prompt → predict token → append → predict token → append → ... → stop</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Every token is another trip through the network. That&#8217;s why latency scales with how much it decides to say.</p>



<p class="wp-block-paragraph">Laya does this:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">read prompt (with [MASK] markers where the answers go) → ONE pass → read probabilities</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The answer positions are marked in the input. After a single pass, the probabilities for every one of your questions are sitting right there in the output tensor. There&#8217;s no sampling, no temperature roulette, no &#8220;it decided to be chatty today.&#8221;</p>



<p class="wp-block-paragraph">Which means <strong>batching is nearly free</strong>. Four questions get four sets of markers in the same sequence, and you still pay for one pass.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Show me the code</h2>



<p class="wp-block-paragraph">Minimal version — one question:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">LocalLLMs</span>.<span class="tok-variableName">Decisions</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">client</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">LayaOnnxDecisionClient</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">result</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">client</span>.<span class="tok-variableName">ChooseAsync</span>(</div><div class="cm-line">    <span class="tok-string">&quot;My invoice charged me twice for the same subscription month and I want my money back.&quot;</span>,</div><div class="cm-line">    <span class="tok-keyword">new</span>[] { <span class="tok-string">&quot;billing&quot;</span>, <span class="tok-string">&quot;technical&quot;</span>, <span class="tok-string">&quot;sales&quot;</span> },</div><div class="cm-line">    <span class="tok-string">&quot;Which team should handle this support ticket?&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">result</span>.<span class="tok-variableName">Choice</span>);        <span class="tok-comment">// billing</span></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">result</span>.<span class="tok-variableName">Probability</span>);   <span class="tok-comment">// 0.984</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">First run downloads the model from HuggingFace (<strong>~800 MB</strong>) and caches it. Every run after that is local and offline.</p>



<h3 class="wp-block-heading">Option descriptions matter more than you&#8217;d think</h3>



<p class="wp-block-paragraph">This is the single highest-leverage thing in the whole API, and it&#8217;s easy to miss. Instead of bare labels, pass a dictionary with <strong>descriptions</strong>:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">teams</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">Dictionary</span><span class="tok-operator">&lt;</span><span class="tok-typeName">string</span>, <span class="tok-typeName">string</span><span class="tok-operator">?&gt;</span></div><div class="cm-line">{</div><div class="cm-line">    [<span class="tok-string">&quot;billing&quot;</span>]   <span class="tok-operator">=</span> <span class="tok-string">&quot;Payment, invoice and refund problems&quot;</span>,</div><div class="cm-line">    [<span class="tok-string">&quot;technical&quot;</span>] <span class="tok-operator">=</span> <span class="tok-string">&quot;Bugs, outages, API errors and anything with a stack trace&quot;</span>,</div><div class="cm-line">    [<span class="tok-string">&quot;sales&quot;</span>]     <span class="tok-operator">=</span> <span class="tok-string">&quot;Pricing, plan comparisons, upgrades and new purchases&quot;</span></div><div class="cm-line">};</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The word <code>technical</code> on its own is nearly meaningless to a classifier. <em>&#8220;Bugs, outages, API errors and anything with a stack trace&#8221;</em> gives it something to actually match against. The accuracy difference is not subtle. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f3af.png" alt="🎯" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<h3 class="wp-block-heading">The real thing: 4 questions, 1 pass</h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">result</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">client</span>.<span class="tok-variableName">EvaluateAsync</span>(</div><div class="cm-line">    <span class="tok-keyword">new</span> <span class="tok-variableName">DecisionRequest</span>(<span class="tok-variableName">ticket</span>)</div><div class="cm-line">        .<span class="tok-variableName">Choose</span>(<span class="tok-string">&quot;team&quot;</span>, <span class="tok-variableName">teams</span>, <span class="tok-string">&quot;Which team should handle this support ticket?&quot;</span>)</div><div class="cm-line">        .<span class="tok-variableName">Score</span>(<span class="tok-string">&quot;urgency&quot;</span>, <span class="tok-variableName">urgencyLevels</span>, <span class="tok-string">&quot;How urgent is this ticket?&quot;</span>)</div><div class="cm-line">        .<span class="tok-variableName">Ask</span>(<span class="tok-string">&quot;refund&quot;</span>, <span class="tok-string">&quot;The customer is asking for a refund.&quot;</span>)</div><div class="cm-line">        .<span class="tok-variableName">Ask</span>(<span class="tok-string">&quot;angry&quot;</span>,  <span class="tok-string">&quot;The customer is angry or frustrated.&quot;</span>));</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">ChoiceResult</span>      <span class="tok-variableName">team</span>    <span class="tok-operator">=</span> <span class="tok-variableName">result</span>.<span class="tok-variableName">Choice</span>(<span class="tok-string">&quot;team&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">ScoreResult</span>       <span class="tok-variableName">urgency</span> <span class="tok-operator">=</span> <span class="tok-variableName">result</span>.<span class="tok-variableName">Score</span>(<span class="tok-string">&quot;urgency&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">ProbabilityResult</span> <span class="tok-variableName">refund</span>  <span class="tok-operator">=</span> <span class="tok-variableName">result</span>.<span class="tok-variableName">Probability</span>(<span class="tok-string">&quot;refund&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">ProbabilityResult</span> <span class="tok-variableName">angry</span>   <span class="tok-operator">=</span> <span class="tok-variableName">result</span>.<span class="tok-variableName">Probability</span>(<span class="tok-string">&quot;angry&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-comment">// Gate on the probability, not the label — so ambiguous tickets go to a human</span></div><div class="cm-line"><span class="tok-comment">// instead of being confidently misrouted.</span></div><div class="cm-line"><span class="tok-typeName">string</span> <span class="tok-variableName">route</span> <span class="tok-operator">=</span> <span class="tok-variableName">team</span>.<span class="tok-variableName">ChoiceOrNull</span>(<span class="tok-number">0.6</span>) <span class="tok-operator">??</span> <span class="tok-string">&quot;human-review&quot;</span>;</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Here&#8217;s the <strong>actual output</strong> from the <a href="../../src/samples/LocalDecisions/"><code>LocalDecisions</code> sample</a> on my machine:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">------------------------------------------------------------------------------</div><div class="cm-line">My invoice charged me twice for the same subscription month and I want my m…</div><div class="cm-line"></div><div class="cm-line">  route   : billing        (billing @ 98.4%)</div><div class="cm-line">  urgency : urgent, answer within an hour</div><div class="cm-line">            score 1.67 of 3</div><div class="cm-line">  refund  : yes  (p = 0.940)</div><div class="cm-line">  angry   : yes  (p = 0.768)</div><div class="cm-line"></div><div class="cm-line">  distribution: billing 98%  sales 1%  technical 1%</div><div class="cm-line">  244 input tokens, 586 ms, model &apos;elbruno/laya-onnx&apos;</div><div class="cm-line">------------------------------------------------------------------------------</div><div class="cm-line">The export endpoint returns a 500 every time I pass more than 50 ids. Here …</div><div class="cm-line"></div><div class="cm-line">  route   : technical      (technical @ 73.7%)</div><div class="cm-line">  urgency : urgent, answer within an hour</div><div class="cm-line">            score 1.82 of 3</div><div class="cm-line">  refund  : no   (p = 0.412)</div><div class="cm-line">  angry   : no   (p = 0.002)</div><div class="cm-line"></div><div class="cm-line">  distribution: technical 74%  billing 15%  sales 11%</div><div class="cm-line">  260 input tokens, 595 ms, model &apos;elbruno/laya-onnx&apos;</div><div class="cm-line">------------------------------------------------------------------------------</div><div class="cm-line">Hi! Just wondering what the difference is between the Pro and Team plans be…</div><div class="cm-line"></div><div class="cm-line">  route   : sales          (sales @ 64.9%)</div><div class="cm-line">  urgency : normal, answer within a day</div><div class="cm-line">            score 0.94 of 3</div><div class="cm-line">  refund  : no   (p = 0.000)</div><div class="cm-line">  angry   : no   (p = 0.000)</div><div class="cm-line"></div><div class="cm-line">  distribution: sales 65%  billing 20%  technical 15%</div><div class="cm-line">  248 input tokens, 526 ms, model &apos;elbruno/laya-onnx&apos;</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Look at that middle one. <code>refund: no (p = 0.412)</code> — the model is genuinely unsure, and it <em>tells me</em> it&#8217;s unsure. A chat model would have just said &#8220;no&#8221; with total conviction. Having the number is the whole point. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4ca.png" alt="📊" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">And <code>angry: p = 0.002</code> on the stack-trace ticket versus <code>0.768</code> on the double-billing one. That&#8217;s a sentiment signal I got <strong>for free</strong>, in the same pass, because I asked.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Now the part where I&#8217;m honest with you <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f534.png" alt="🔴" class="wp-smiley" style="height: 1em; max-height: 1em;" /></h2>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/assets/local-decisions/calibration-clamp.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/local-decisions/calibration-clamp.png" alt="A temperature dial clamped by a warning shield, with probability bars collapsing into saturation" /></a></figure>



<p class="wp-block-paragraph">I could stop the post here and it would be a nice launch announcement. But I ran into something while building this that you need to know about, because it would absolutely bite you in production.</p>



<p class="wp-block-paragraph"><strong>Laya&#8217;s public checkpoints are not reliably calibrated.</strong></p>



<p class="wp-block-paragraph">Laya fits a separate <em>temperature</em> for each <code>(question type, option count)</code> bucket — <code>choice:2</code>, <code>choice:3-5</code>, <code>choice:6-10</code>, <code>choice:11+</code>. That temperature divides the logits before the softmax.</p>



<p class="wp-block-paragraph">The English checkpoint ships <strong><code>choice:11+ = 0.1006</code></strong>.</p>



<p class="wp-block-paragraph">Laya&#8217;s own code says the minimum valid temperature is <code>0.5</code>. Dividing by <code>0.1006</code> multiplies the logits roughly <strong>tenfold</strong> before the softmax — which flattens every distribution into a spike at whatever won.</p>



<p class="wp-block-paragraph">So this package clamps it to <code>0.5</code> and — this is the important bit — <strong>tells you it did</strong>:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">result</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">client</span>.<span class="tok-variableName">ChooseAsync</span>(<span class="tok-variableName">ticket</span>, <span class="tok-variableName">twelveDepartments</span>, <span class="tok-string">&quot;Route this ticket&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">if</span> (<span class="tok-variableName">result</span>.<span class="tok-variableName">CalibrationClamped</span>)</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-comment">// The ranking is still meaningful. The confidence number is not.</span></div><div class="cm-line">    <span class="tok-variableName">logger</span>.<span class="tok-variableName">LogWarning</span>(<span class="tok-string">&quot;Confidence for {Choice} is not a fitted calibration.&quot;</span>, <span class="tok-variableName">result</span>.<span class="tok-variableName">Choice</span>);</div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">It&#8217;s a <strong>per-answer</strong> flag, not a global one, because a single checkpoint can have perfectly good temperatures for some buckets and garbage for others.</p>



<h3 class="wp-block-heading">But here&#8217;s what the clamp does <em>not</em> buy you</h3>



<p class="wp-block-paragraph">I wrote in the docs, at first, that clamping &#8220;keeps the number honest.&#8221; Then I actually ran it, and I was wrong. So I went back and fixed the docs — and the HuggingFace model card. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f62c.png" alt="😬" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">Same ticket, same model, only the number of options changes:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Options</th><th>Bucket</th><th>Choice</th><th>Reported confidence</th><th><code>CalibrationClamped</code></th></tr></thead><tbody><tr><td>5</td><td><code>choice:3-5</code></td><td><code>billing</code></td><td>86.9%</td><td><code>false</code></td></tr><tr><td>12</td><td><code>choice:11+</code></td><td><code>billing</code></td><td><strong>100.0%</strong></td><td><code>true</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Both picked the <strong>right</strong> department. But the 12-option answer reports <strong>100.0% confidence</strong> — <em>after</em> clamping. Because <code>0.5</code> still doubles the logits. Clamping prevents the catastrophic tenfold sharpening; it does not give you a calibrated number.</p>



<p class="wp-block-paragraph">What that means in practice:</p>



<ul class="wp-block-list">
<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Use the ranking.</strong> <code>Choice</code> and the relative order of <code>Probabilities</code> are trustworthy.</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f534.png" alt="🔴" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Don&#8217;t use the magnitude</strong> for choices with 11+ options.</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f534.png" alt="🔴" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong><code>ChoiceOrNull(threshold)</code> will never abstain</strong> in that bucket — a saturated probability clears every threshold you&#8217;d plausibly set.</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Prefer fewer options.</strong> Splitting one 12-way choice into two smaller ones keeps you in a bucket the checkpoint actually fitted correctly.</li>
</ul>



<h3 class="wp-block-heading">The fp16 gotcha</h3>



<p class="wp-block-paragraph">One more, smaller one. The checkpoint stores <strong>fp16</strong> weights. Ask a question alone vs. batched with three others and you get <em>slightly</em> different numbers, because batched matrix multiplications accumulate in a different order:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Label</th><th>Alone</th><th>Batched with 3 others</th><th>Delta</th></tr></thead><tbody><tr><td><code>billing</code></td><td>0.93738423</td><td>0.93739642</td><td>1.22e-05</td></tr><tr><td><code>sales</code></td><td>0.03319702</td><td>0.03318152</td><td>1.55e-05</td></tr><tr><td><code>technical</code></td><td>0.02941875</td><td>0.02942205</td><td>3.31e-06</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Deltas land around <code>1e-6</code> to <code>1e-4</code>. Way too small to flip a routing decision. But big enough to ruin your day if you <strong>assert exact equality in tests</strong> (use a <code>1e-3</code> tolerance), <strong>cache on the probability value</strong> (key on the label instead), or <strong>hash results for change detection</strong> (round first).</p>



<p class="wp-block-paragraph">Batching is still the right default. Just don&#8217;t treat the low decimals as stable.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">What I had to build to make this work in .NET</h2>



<p class="wp-block-paragraph">There was no ONNX build of Laya when I started. There is now — I ported one and published it as <strong><a href="https://huggingface.co/elbruno/laya-onnx"><code>elbruno/laya-onnx</code></a></strong> (mirroring the Apache-2.0 original).</p>



<p class="wp-block-paragraph">The .NET side had to reimplement, faithfully, what the Python reference does:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Piece</th><th>What it does</th></tr></thead><tbody><tr><td><strong>Tokenizer</strong></td><td><code>Microsoft.ML.Tokenizers</code> — no Python tokenizer bridge</td></tr><tr><td><strong>Sequence builder</strong></td><td>Lays out the prompt with <code>[MASK]</code> markers at every answer position</td></tr><tr><td><strong>Temperature config</strong></td><td>Reads the per-bucket fitted temperatures — and clamps the invalid ones</td></tr><tr><td><strong>Decode math</strong></td><td>Pulls logits at the marker offsets, applies temperature, softmaxes</td></tr><tr><td><strong>Runtime</strong></td><td><code>Microsoft.ML.OnnxRuntime</code>, in-process, CPU</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The whole thing is covered by <strong>79 tests</strong>, including parity checks against the reference outputs — because &#8220;I ported the math correctly&#8221; is a claim that deserves evidence, not vibes. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f9ea.png" alt="🧪" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">Wiring it into DI is one line:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-variableName">builder</span>.<span class="tok-variableName">Services</span>.<span class="tok-variableName">AddLocalDecisions</span>();</div></code></pre>
		</div>
	</div>
</div>


<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">When to use this (and when not to)</h2>



<p class="wp-block-paragraph"><strong>Reach for it when:</strong></p>



<ul class="wp-block-list">
<li>You&#8217;re classifying, routing, triaging, moderating, or scoring <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4e5.png" alt="📥" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>



<li>You need the answer <strong>fast</strong> and <strong>cheap</strong> and <strong>offline</strong></li>



<li>You want a <strong>probability</strong>, not a sentence</li>



<li>You&#8217;re asking the same shaped question thousands of times</li>
</ul>



<p class="wp-block-paragraph"><strong>Don&#8217;t reach for it when:</strong></p>



<ul class="wp-block-list">
<li>You need something written, summarised, or explained</li>



<li>The task genuinely needs multi-step reasoning</li>



<li>You need calibrated confidence over 11+ options and can&#8217;t fit your own temperature</li>
</ul>



<p class="wp-block-paragraph">It&#8217;s not a replacement for a chat model. It&#8217;s the thing you put <strong>in front of</strong> one, so the expensive model only sees the cases that actually need it. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f6a6.png" alt="🚦" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Getting started</h2>



<ol class="wp-block-list">
<li><strong>Install:</strong><code>dotnet add package ElBruno.LocalLLMs.Decisions</code></li>



<li><strong>Run a sample</strong> — the model downloads and caches on first use:<code>dotnet run --project src/samples/LocalDecisions</code></li>



<li><strong>See the caveats live:</strong><code>dotnet run --project src/samples/DecisionCalibration</code></li>



<li><strong>Fit your own threshold</strong> on your own labelled data. The <code>0.5</code> default is a starting point, not a decision boundary.</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">What&#8217;s next</h2>



<ul class="wp-block-list">
<li>Surfacing the <strong>escalation head</strong> (<code>act_logits</code>) — Laya has a signal for &#8220;this needs a human&#8221; that I read but don&#8217;t expose yet</li>



<li><strong>int8</strong> export and a proper benchmark against the fp16 one</li>



<li><strong>Multilingual checkpoints</strong> — the English one stays confident on non-Latin scripts while being wrong, which is the worst combination</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<p class="wp-block-paragraph">Building this changed how I think about where LLMs belong in an app. Not every question deserves a conversation. Some questions just need an answer, in 600 milliseconds, on your own machine, with a number attached so you know how much to trust it.</p>



<p class="wp-block-paragraph">And honestly? Finding that <code>0.1006</code> temperature — and then discovering my own fix didn&#8217;t fully fix it — was the most fun part. That&#8217;s the difference between shipping a wrapper and shipping something you&#8217;ve actually <em>looked inside</em>. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph"><strong>— Bruno</strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4cc.png" alt="📌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>Full guide: <a href="../decisions-guide.md">Decisions Guide</a> · Samples: <a href="../../src/samples/LocalDecisions/">LocalDecisions</a> · <a href="../../src/samples/DecisionCalibration/">DecisionCalibration</a> · Model: <a href="https://huggingface.co/elbruno/laya-onnx">elbruno/laya-onnx</a> · Original: <a href="https://github.com/NandhaKishorM/laya">NandhaKishorM/laya</a></em></p>



<p class="wp-block-paragraph"><em>Made with <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2764.png" alt="❤" class="wp-smiley" style="height: 1em; max-height: 1em;" /> by <a href="https://elbruno.com">Bruno Capuano (ElBruno)</a></em></p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/10/01/%e2%9a%a1i-stopped-asking-an-llm-which-team-owns-this-ticket-now-i-get-an-answer-in-600ms-locally/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">43734</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/08/image-4.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/08/image-4.png">
			<media:title type="html">image</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/images/blog-local-decisions.png">
			<media:title type="html">Local decisions — System One fast intuition versus System Two deliberate reasoning, with a laptop running C# in the middle</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/local-decisions/one-forward-pass.png">
			<media:title type="html">A single arrow passing through a neural network in one sweep, emerging as probability bars, contrasted with a looping generation spiral</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/local-decisions/calibration-clamp.png">
			<media:title type="html">A temperature dial clamped by a warning shield, with probability bars collapsing into saturation</media:title>
		</media:content>
	</item>
		<item>
		<title>The Aspire Demo We Didn’t Get to Show Live: Watching an Agent Harness Work</title>
		<link>https://elbruno.com/2026/09/25/the-aspire-demo-we-didnt-get-to-show-live-watching-an-agent-harness-work/</link>
					<comments>https://elbruno.com/2026/09/25/the-aspire-demo-we-didnt-get-to-show-live-watching-an-agent-harness-work/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Fri, 25 Sep 2026 17:31:43 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[Agent Harnesses]]></category>
		<category><![CDATA[AI Agents]]></category>
		<category><![CDATA[Aspire]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[MAF]]></category>
		<category><![CDATA[Microsoft Agent Framework]]></category>
		<category><![CDATA[Observability]]></category>
		<category><![CDATA[OpenTelemetry]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=43727</guid>

					<description><![CDATA[The Aspire Demo We Didn&#8217;t Get to Show Live: Watching an Agent Harness Work We ran out of time during Session 4 of MafClaw to show the Aspire dashboard demo. And this is one of those demos that really benefits from seeing it, not just hearing &#8220;yes, we have observability.&#8221; So here is the walkthrough [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><a href="https://github.com/elbruno/mafclaw-plan/blob/main/campaign/blog/2026-09-24-mafclaw-aspire-observability.md#the-aspire-demo-we-didnt-get-to-show-live-watching-an-agent-harness-work"></a></p>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<h1 class="wp-block-heading">The Aspire Demo We Didn&#8217;t Get to Show Live: Watching an Agent Harness Work</h1>



<p class="wp-block-paragraph">We ran out of time during Session 4 of MafClaw to show the Aspire dashboard demo. And this is one of those demos that really benefits from seeing it, not just hearing &#8220;yes, we have observability.&#8221;</p>



<p class="wp-block-paragraph">So here is the walkthrough we didn&#8217;t get to do live: take a small C# agent, let Microsoft Agent Framework&#8217;s Harness call a tool, and follow that work through <strong>traces, structured logs, and metrics in Aspire</strong>.</p>



<p class="wp-block-paragraph">No finance application to understand first. No AppHost. No Docker requirement. Just a console sample and the standalone dashboard.</p>



<h2 class="wp-block-heading">Start with one question and one tool</h2>



<p class="wp-block-paragraph">Sample 12, <code>12-observability-aspire</code>, asks a deliberately simple question:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">What is this workshop lesson about?</p>
</blockquote>



<p class="wp-block-paragraph">The agent has one ordinary C# function, <code>get_lesson_topic</code>. It returns a synthetic workshop topic. The interesting part is not the answer; it is <strong>what happens between the question and the answer</strong>.</p>



<p class="wp-block-paragraph">The model requests the tool, the Harness executes it and sends its result back, and the model produces the final response. We make one <code>RunAsync</code> call. We do not write that model/tool loop ourselves.</p>



<p class="wp-block-paragraph">This is a new variant, not a replacement for the earlier observability samples:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Sample</th><th>What it teaches</th></tr></thead><tbody><tr><td>10</td><td>Instrument an ordinary C# request and tool with spans and a counter.</td></tr><tr><td>11</td><td>Observe the real MAF/Harness agent, model, and tool spans in the console.</td></tr><tr><td>12</td><td>Export the generic agent&#8217;s telemetry and explore it in Aspire.</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The complete finance application is still the final example, after these concepts have been introduced on their own.</p>



<h2 class="wp-block-heading">Two terminals, then <code>dotnet run</code></h2>



<p class="wp-block-paragraph">You need .NET 10, the <a href="https://aspire.dev/get-started/install-cli/" target="_blank" rel="noopener">Aspire CLI</a>, and the MafClaw source. This walkthrough uses Aspire CLI 13.5.1. The normal sample uses your configured Microsoft Foundry model, so complete the shared setup once from the repository root:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-punctuation">.\</span><span class="tok-variableName">tools</span><span class="tok-punctuation">\</span><span class="tok-variableName">configure-user-secrets</span><span class="tok-punctuation">.</span><span class="tok-variableName">ps1</span></div><div class="cm-line"><span class="tok-variableName">az</span> <span class="tok-variableName">login</span> <span class="tok-operator">--</span><span class="tok-variableName">output</span> <span class="tok-variableName">none</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">If your Session 4 model settings are already configured, do not set them again just for this sample. It shares the existing settings store. Live model inference can incur charges. In the first terminal:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">aspire</span> <span class="tok-variableName">dashboard</span> <span class="tok-variableName">run</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Open the login URL printed by the CLI and keep its browser token private. The default browser endpoint is <code>http://localhost:18888</code>; the OTLP/HTTP receiver is on h<code>ttp://localhost:4318</code>. They are different ports for different jobs.</p>



<p class="wp-block-paragraph">In the second terminal, from the public repository root:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">cd</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">session-04</span><span class="tok-punctuation">\</span><span class="tok-variableName">samples</span><span class="tok-punctuation">\</span><span class="tok-number">12</span><span class="tok-operator">-</span><span class="tok-variableName">observability-aspire</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">run</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That&#8217;s the demo command. No <code>--live</code>, no save/resume arguments, and no dashboard configuration file to write.</p>



<p class="wp-block-paragraph">Select <strong>mafclaw-sample12</strong> in the dashboard. The console prints the trace ID so you can find the exact run instead of guessing which request to open.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>About the screenshots:</strong> these were captured after the session, not during the broadcast. They use explicitly scripted model responses (<code>--fixture</code>) to keep the screenshots reproducible and free of live deployment identifiers. The Harness, C# tool execution, OpenTelemetry export, and Aspire records are real. The short timings are <strong>not</strong> live model latency or a performance benchmark.</p>
</blockquote>



<h2 class="wp-block-heading">1. Traces: who did what?</h2>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="369" src="https://elbruno.com/wp-content/uploads/2026/09/image.png?w=1024" alt="" class="wp-image-43729" srcset="https://elbruno.com/wp-content/uploads/2026/09/image.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/09/image.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/09/image.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/09/image.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/09/image.png 1440w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><em>Post-session fixture capture: five real spans, including the model/tool/model sequence inside the Harness invocation.</em></p>



<p class="wp-block-paragraph">Expand the trace and read it from the outside in:</p>



<ul class="wp-block-list">
<li><code>lesson.run</code> is the sample&#8217;s root operation.</li>



<li><code>invoke_agent</code> is the MAF agent invocation.</li>



<li>The first <code>chat</code> is the model turn that asks to use the tool.</li>



<li><code>execute_tool get_lesson_topic</code> is the actual C# tool execution.</li>



<li>The second <code>chat</code> processes the returned result.</li>
</ul>



<p class="wp-block-paragraph">That is the part I wanted to pause on during the stream. The final answer doesn&#8217;t tell us whether a function actually ran. The trace gives us the execution path and the parent/child relationships.</p>



<p class="wp-block-paragraph">In a live run, this also helps separate model latency from tool latency. A fixture still proves orchestration, but its near-instant model turns cannot tell us how fast the configured model will be.</p>



<h2 class="wp-block-heading">2. Structured logs: give a message its context</h2>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="369" src="https://elbruno.com/wp-content/uploads/2026/09/image-1.png?w=1024" alt="" class="wp-image-43730" srcset="https://elbruno.com/wp-content/uploads/2026/09/image-1.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/09/image-1.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/09/image-1.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/09/image-1.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/09/image-1.png 1440w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><em>The completion log links back to the same trace shown above.</em></p>



<p class="wp-block-paragraph">The application emits one intentionally boring message:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-variableName">logger</span>.<span class="tok-variableName">LogInformation</span>(<span class="tok-string">&quot;Lesson completed with {ToolCalls} tool call.&quot;</span>, <span class="tok-variableName">toolCalls</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Because that log is written while <code>lesson.run</code> is active, it carries the trace and span identifiers. Click the trace link in Aspire and you return to the work that produced the message.</p>



<p class="wp-block-paragraph">This is much more useful than a disconnected &#8220;done!&#8221; line. The log answers &#8220;what did the application report?&#8221; and the trace lets us investigate &#8220;what happened around it?&#8221;</p>



<p class="wp-block-paragraph">Also notice what the message does <strong>not</strong> contain: the user prompt, the model&#8217;s answer, or the tool payload.</p>



<h2 class="wp-block-heading">3. Metrics: count the behavior, not the prose</h2>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="369" src="https://elbruno.com/wp-content/uploads/2026/09/image-2.png?w=1024" alt="" class="wp-image-43731" srcset="https://elbruno.com/wp-content/uploads/2026/09/image-2.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/09/image-2.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/09/image-2.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/09/image-2.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/09/image-2.png 1440w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><em>The counter is incremented inside the real C# tool. Aspire&#8217;s Table view shows a value of 1 for this run.</em></p>



<p class="wp-block-paragraph">The counter is small enough to explain without leaving the file:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">toolCounter</span> <span class="tok-operator">=</span> <span class="tok-variableName">meter</span>.<span class="tok-variableName">CreateCounter</span><span class="tok-operator">&lt;</span><span class="tok-typeName">long</span><span class="tok-operator">&gt;</span>(</div><div class="cm-line">    <span class="tok-string">&quot;lesson.tool.calls&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">description</span>: <span class="tok-string">&quot;Actual lesson-tool invocations.&quot;</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Inside <code>GetLessonTopic</code>, we call:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-variableName">toolCounter</span>.<span class="tok-variableName">Add</span>(<span class="tok-number">1</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">This is a measurement of an invocation, not the model claiming that it called a tool. Each run of this short console sample creates a fresh in-process counter; it is not a durable total across process restarts.</p>



<p class="wp-block-paragraph">Aspire also lists SDK instruments such as operation duration and token usage when available. Missing token data does not mean zero tokens. Scripted inference is not evidence of real model usage or cost.</p>



<h2 class="wp-block-heading">Where the instrumentation comes from</h2>



<p class="wp-block-paragraph">The integration is visible in <code>Program.cs</code>, rather than hidden behind the final application&#8217;s factory. These two excerpts show the important boundaries:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">observedModel</span> <span class="tok-operator">=</span> <span class="tok-variableName">model</span>.<span class="tok-variableName">AsBuilder</span>()</div><div class="cm-line">    .<span class="tok-variableName">UseOpenTelemetry</span>(</div><div class="cm-line">        <span class="tok-variableName">sourceName</span>: <span class="tok-variableName">sourceName</span>,</div><div class="cm-line">        <span class="tok-variableName">configure</span>: <span class="tok-variableName">telemetry</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">telemetry</span>.<span class="tok-variableName">EnableSensitiveData</span> <span class="tok-operator">=</span> <span class="tok-atom">false</span>)</div><div class="cm-line">    .<span class="tok-variableName">Build</span>();</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph"><code>Microsoft.Extensions.AI</code> observes individual model calls. We use that client to construct the Harness with <code>AsHarnessAgent</code>, then instrument the outer agent invocation:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">agent</span> <span class="tok-operator">=</span> <span class="tok-variableName">harness</span>.<span class="tok-variableName">AsBuilder</span>()</div><div class="cm-line">    .<span class="tok-variableName">UseOpenTelemetry</span>(</div><div class="cm-line">        <span class="tok-variableName">sourceName</span>,</div><div class="cm-line">        <span class="tok-variableName">telemetry</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">telemetry</span>.<span class="tok-variableName">EnableSensitiveData</span> <span class="tok-operator">=</span> <span class="tok-atom">false</span>)</div><div class="cm-line">    .<span class="tok-variableName">Build</span>();</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The Harness&#8217;s automatic telemetry wrapper is disabled in this sample so the explicit agent wrapper is the one we teach, not a duplicate. Tool invocation instrumentation comes from the SDK. OpenTelemetry then subscribes to those sources and exports the data:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">traces</span> <span class="tok-operator">=</span> <span class="tok-variableName">Sdk</span>.<span class="tok-variableName">CreateTracerProviderBuilder</span>()</div><div class="cm-line">    .<span class="tok-variableName">SetResourceBuilder</span>(<span class="tok-variableName">resource</span>)</div><div class="cm-line">    .<span class="tok-variableName">AddSource</span>(<span class="tok-variableName">sourceName</span>, <span class="tok-string">&quot;Microsoft.Extensions.AI&quot;</span>)</div><div class="cm-line">    .<span class="tok-variableName">AddOtlpExporter</span>(<span class="tok-variableName">options</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">ConfigureExport</span>(<span class="tok-variableName">options</span>, <span class="tok-string">&quot;traces&quot;</span>))</div><div class="cm-line">    .<span class="tok-variableName">Build</span>();</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The metric and log providers use the same service name. The exporter helper sends HTTP/protobuf to <code>/v1/traces</code>, <code>/v1/metrics</code>, and <code>/v1/logs</code> on the local receiver.</p>



<p class="wp-block-paragraph">So the responsibilities are separate: <strong>the Harness orchestrates, the SDK instruments, OpenTelemetry exports, and Aspire displays.</strong> Starting the dashboard alone does not instrument an application.</p>



<h2 class="wp-block-heading">Two details that matter in a short demo</h2>



<p class="wp-block-paragraph"><strong>Flush before the process exits.</strong> A console program can finish before batched telemetry has been sent. The sample explicitly flushes traces and metrics and disposes the log provider. It also observes exporter failures. A successful answer followed by a failed export is not reported as an observability success.</p>



<p class="wp-block-paragraph"><code>ASPIRE EXPORT PASS</code> means the run and flushes completed without a reported exporter failure. It is still worth opening Aspire and checking the retained records, as we did here.</p>



<p class="wp-block-paragraph"><strong>Keep content capture off, but don&#8217;t call that complete redaction.</strong> Both wrappers set <code>EnableSensitiveData = false</code>. Operational metadata, including model identifiers or server addresses in live runs, can still appear. Use synthetic questions, keep the dashboard local, and inspect screenshots before sharing them.</p>



<p class="wp-block-paragraph">The screenshots came from a separate loopback-only dashboard with anonymous access for the capture. That is why Aspire&#8217;s unsecured-endpoint warning is visible. The normal <code>aspire dashboard run</code> command above keeps its default browser-token protection; do not expose an anonymous dashboard to a network.</p>



<h2 class="wp-block-heading">If you see an answer but no telemetry</h2>



<p class="wp-block-paragraph">First, make sure the dashboard was started before the sample. Then check that the exporter points at <strong>4318</strong>, not the UI&#8217;s <strong>18888</strong>. A stale <code>OTEL_EXPORTER_OTLP_ENDPOINT</code> environment variable can override the sample&#8217;s default; clear it or set it to the matching loopback HTTP base address.</p>



<p class="wp-block-paragraph">The standalone dashboard stores telemetry in memory. Stopping the console doesn&#8217;t remove the exported run, but restarting the dashboard does. It is a telemetry viewer here, not an AppHost managing the console process.</p>



<p class="wp-block-paragraph">For an offline rehearsal, <code>dotnet run -- --fixture</code> uses scripted inference while keeping the real tool and telemetry pipeline. It still needs a running dashboard or collector. That switch is for reproducible checks; the normal teaching flow remains <code>dotnet run</code>.</p>



<h2 class="wp-block-heading">The demo&#8217;s real takeaway</h2>



<p class="wp-block-paragraph">An agent&#8217;s answer is the beginning of the investigation, not the whole story. With this small sample we can see which component called the tool, how the model turns fit together, and whether the log and counter agree with the trace.</p>



<p class="wp-block-paragraph">We didn&#8217;t get to show that live, but now you can run it at your own pace: start the dashboard, run the sample, and follow one request all the way through.</p>



<p class="wp-block-paragraph">The series materials are in the <a href="https://aka.ms/mafclaw/repo">MafClaw repository</a>.<br />See the <a href="../../public-staging/session-04/samples/12-observability-aspire/README.md">Sample 12 source and walkthrough</a><br />and the <a href="https://aspire.dev/dashboard/standalone/">Aspire standalone dashboard documentation</a><br />for the complete setup and troubleshooting details.</p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/09/25/the-aspire-demo-we-didnt-get-to-show-live-watching-an-agent-harness-work/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">43727</post-id>
		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/09/image.png?w=1024"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/09/image-1.png?w=1024"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/09/image-2.png?w=1024"/>
	</item>
		<item>
		<title>What Happens When Nobody Approves the Agent?</title>
		<link>https://elbruno.com/2026/09/10/what-happens-when-nobody-approves-the-agent/</link>
					<comments>https://elbruno.com/2026/09/10/what-happens-when-nobody-approves-the-agent/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Thu, 10 Sep 2026 17:33:50 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[Agent Harness]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Harness]]></category>
		<category><![CDATA[MAF]]></category>
		<category><![CDATA[Microsoft Agent Framework]]></category>
		<category><![CDATA[Microsoft Foundry]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=43724</guid>

					<description><![CDATA[Hi! During the live Q&#38;A in the second session of the&#160;From Model to Agent: The Agent Framework Harness, Live in C#&#160;series on September 10, 2026, someone in the audience asked a practical question: What happens if an agent requests approval, but the user does not answer? Can the approval expire or retry? That question is [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">During the live Q&amp;A in the second session of the&nbsp;<strong>From Model to Agent: The Agent Framework Harness, Live in C#</strong>&nbsp;series on September 10, 2026, someone in the audience asked a practical question:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">What happens if an agent requests approval, but the user does not answer? Can the approval expire or retry?</p>
</blockquote>



<p class="wp-block-paragraph">That question is easy to overlook in a happy-path demo. We usually show the agent requesting a tool, the user selecting&nbsp;<strong>Approve</strong>&nbsp;or&nbsp;<strong>Deny</strong>, and the workflow continuing. Real applications also need to handle silence, invalid answers, repeated approval requests, cancellation, and unavailable input.</p>



<p class="wp-block-paragraph">So, after the session, I created a new sample:&nbsp;<strong>22 &#8211; Approval Retries and Timeouts</strong>.</p>



<p class="wp-block-paragraph">The sample adds a bounded approval policy to a Microsoft Agent Framework agent running with Harness:</p>



<ul class="wp-block-list">
<li>five seconds to answer each approval request</li>



<li>five attempts maximum</li>



<li><code>y</code> or <code>yes</code> approves immediately</li>



<li><code>n</code> or <code>no</code> denies immediately</li>



<li>missing or invalid input retries</li>



<li>cancellation and input failures deny the action</li>



<li>exhausting the attempts denies automatically</li>



<li>one denial remains in effect for the rest of the prompt</li>



<li>repeated approval rounds from the model are also bounded</li>
</ul>



<p class="wp-block-paragraph">The important principle is simple:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Silence is not consent.</p>
</blockquote>



<h2 class="wp-block-heading">About the MafClaw series</h2>



<p class="wp-block-paragraph">MafClaw is a four-part Microsoft Reactor live-coding series where we build a C# agent incrementally with .NET, Microsoft Agent Framework, and Harness.</p>



<p class="wp-block-paragraph">The running sample is a finance education assistant. We start with a model, turn it into an agent, add tools and planning, introduce safe access to data, require human approval for side effects, add memory, and then move toward more advanced and production-ready scenarios.</p>



<ul class="wp-block-list">
<li><a href="https://aka.ms/mafclaw" target="_blank" rel="noopener">Microsoft Reactor series page</a></li>



<li><a href="https://aka.ms/mafclaw/repo" target="_blank" rel="noopener">Sample repository</a></li>



<li><a href="https://aka.ms/mafclaw/blog" target="_blank" rel="noopener">Series introduction</a></li>
</ul>



<p class="wp-block-paragraph">You can also follow the four sessions on the&nbsp;<a href="https://www.youtube.com/@dotnet" target="_blank" rel="noopener">.NET YouTube channel</a>:</p>



<ol class="wp-block-list">
<li><a href="https://www.youtube.com/watch?v=iUs15X1v2w4" target="_blank" rel="noopener">Meet Your Claw: A Harness in Three Lines of C#</a></li>



<li><a href="https://www.youtube.com/watch?v=V58coa0llUo" target="_blank" rel="noopener">Working With Your Data, Safely: Files, Approvals and Memory</a></li>



<li><a href="https://www.youtube.com/watch?v=dCIBza-WxUc" target="_blank" rel="noopener">Scaling the Claw: Skills, Shell, CodeAct and Background Agents</a></li>



<li><a href="https://www.youtube.com/watch?v=rMhX0-oE4aY" target="_blank" rel="noopener">Production Ready: Observability, Governance and Deployment</a></li>
</ol>



<p class="wp-block-paragraph">The audience question that inspired this sample came from&nbsp;<a href="https://www.youtube.com/watch?v=V58coa0llUo" target="_blank" rel="noopener">session 2</a>, where we explored file boundaries, human approval, and agent memory.</p>



<h2 class="wp-block-heading">The scenario</h2>



<p class="wp-block-paragraph">The user asks the agent:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Buy 10 shares of MSFT.</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">This is a simulated trade. No real transaction is created, but it represents a side effect that should not execute only because a model requested it.</p>



<p class="wp-block-paragraph">The tool is wrapped with&nbsp;<code>ApprovalRequiredAIFunction</code>:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">internal</span> <span class="tok-keyword">static</span> <span class="tok-keyword">class</span> <span class="tok-variableName tok-definition">ApprovalGateTools</span></div><div class="cm-line">{</div><div class="cm-line">    [<span class="tok-variableName">Description</span>(<span class="tok-string">&quot;Places a demo-only simulated trade after the Harness approval flow allows it.&quot;</span>)]</div><div class="cm-line">    <span class="tok-keyword">public</span> <span class="tok-keyword">static</span> <span class="tok-typeName">string</span> <span class="tok-variableName">RequestSimulatedTradeOrder</span>(</div><div class="cm-line">        [<span class="tok-variableName">Description</span>(<span class="tok-string">&quot;Trade side, for example buy or sell.&quot;</span>)] <span class="tok-typeName">string</span> <span class="tok-variableName">side</span>,</div><div class="cm-line">        [<span class="tok-variableName">Description</span>(<span class="tok-string">&quot;Ticker symbol, for example MSFT.&quot;</span>)] <span class="tok-typeName">string</span> <span class="tok-variableName">symbol</span>,</div><div class="cm-line">        [<span class="tok-variableName">Description</span>(<span class="tok-string">&quot;Number of shares.&quot;</span>)] <span class="tok-typeName">int</span> <span class="tok-variableName">shares</span>)</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">normalizedSide</span> <span class="tok-operator">=</span> <span class="tok-variableName">side</span>.<span class="tok-variableName">Trim</span>().<span class="tok-variableName">ToLowerInvariant</span>();</div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">normalizedSymbol</span> <span class="tok-operator">=</span> <span class="tok-variableName">symbol</span>.<span class="tok-variableName">Trim</span>().<span class="tok-variableName">ToUpperInvariant</span>();</div><div class="cm-line"></div><div class="cm-line">        <span class="tok-keyword">if</span> (<span class="tok-variableName">shares</span> <span class="tok-operator">&lt;=</span> <span class="tok-number">0</span>)</div><div class="cm-line">        {</div><div class="cm-line">            <span class="tok-keyword">return</span> <span class="tok-string">&quot;Denied: share quantity must be greater than zero.&quot;</span>;</div><div class="cm-line">        }</div><div class="cm-line"></div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">$</span><span class="tok-string">&quot;Approved: simulated {normalizedSide} order for {shares} shares &quot;</span> <span class="tok-operator">+</span></div><div class="cm-line">               <span class="tok-variableName">$</span><span class="tok-string">&quot;of {normalizedSymbol}. This is not a real transaction.&quot;</span>;</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">public</span> <span class="tok-keyword">static</span> <span class="tok-variableName">AIFunction</span> <span class="tok-variableName">RequestSimulatedTrade</span> { <span class="tok-keyword">get</span>; } <span class="tok-operator">=</span></div><div class="cm-line">        <span class="tok-keyword">new</span> <span class="tok-variableName">ApprovalRequiredAIFunction</span>(</div><div class="cm-line">            <span class="tok-variableName">AIFunctionFactory</span>.<span class="tok-variableName">Create</span>(</div><div class="cm-line">                <span class="tok-variableName">RequestSimulatedTradeOrder</span>,</div><div class="cm-line">                <span class="tok-string">&quot;request_simulated_trade&quot;</span>));</div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The model can request&nbsp;<code>request_simulated_trade</code>, but Harness pauses before executing the C# function. The host application, not the model, owns the approval decision.</p>



<h2 class="wp-block-heading">Configure a bounded approval policy</h2>



<p class="wp-block-paragraph">The sample configures the policy with a small block:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">const</span> <span class="tok-typeName">int</span> <span class="tok-variableName">maxApprovalAttempts</span> <span class="tok-operator">=</span> <span class="tok-number">5</span>;</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">approvalTimeout</span> <span class="tok-operator">=</span> <span class="tok-typeName">TimeSpan</span>.<span class="tok-variableName">FromSeconds</span>(<span class="tok-number">5</span>);</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">approvalPolicy</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">TimedApprovalPolicy</span>(</div><div class="cm-line">    <span class="tok-variableName">maxApprovalAttempts</span>,</div><div class="cm-line">    <span class="tok-variableName">approvalTimeout</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">These numbers are intentionally short for a live demo. A production application should choose deadlines and retry limits that match its user experience and the risk of the requested action.</p>



<p class="wp-block-paragraph">The policy validates its configuration:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">public</span> <span class="tok-variableName">TimedApprovalPolicy</span>(</div><div class="cm-line">    <span class="tok-typeName">int</span> <span class="tok-variableName">maxAttempts</span>,</div><div class="cm-line">    <span class="tok-typeName">TimeSpan</span> <span class="tok-variableName">timeout</span>,</div><div class="cm-line">    <span class="tok-typeName">Func</span><span class="tok-operator">&lt;</span><span class="tok-typeName">TimeSpan</span>, <span class="tok-variableName">CancellationToken</span>, <span class="tok-typeName">Task</span><span class="tok-operator">&lt;</span><span class="tok-typeName">string</span><span class="tok-operator">?&gt;&gt;?</span> <span class="tok-variableName">readLineAsync</span> <span class="tok-operator">=</span> <span class="tok-atom">null</span>)</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-variableName">maxAttempts</span> <span class="tok-operator">&lt;=</span> <span class="tok-number">0</span>)</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">throw</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ArgumentOutOfRangeException</span>(</div><div class="cm-line">            <span class="tok-variableName">nameof</span>(<span class="tok-variableName">maxAttempts</span>),</div><div class="cm-line">            <span class="tok-string">&quot;Approval attempts must be greater than zero.&quot;</span>);</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-variableName">timeout</span> <span class="tok-operator">&lt;=</span> <span class="tok-typeName">TimeSpan</span>.<span class="tok-variableName">Zero</span>)</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">throw</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ArgumentOutOfRangeException</span>(</div><div class="cm-line">            <span class="tok-variableName">nameof</span>(<span class="tok-variableName">timeout</span>),</div><div class="cm-line">            <span class="tok-string">&quot;Approval timeout must be greater than zero.&quot;</span>);</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">this</span>.<span class="tok-variableName">maxAttempts</span> <span class="tok-operator">=</span> <span class="tok-variableName">maxAttempts</span>;</div><div class="cm-line">    <span class="tok-keyword">this</span>.<span class="tok-variableName">timeout</span> <span class="tok-operator">=</span> <span class="tok-variableName">timeout</span>;</div><div class="cm-line">    <span class="tok-keyword">this</span>.<span class="tok-variableName">readLineAsync</span> <span class="tok-operator">=</span> <span class="tok-variableName">readLineAsync</span> <span class="tok-operator">??</span> <span class="tok-variableName">ReadConsoleLineWithTimeoutAsync</span>;</div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The injectable input reader is useful beyond this console sample. It keeps the approval policy separate from the user interface and makes the decision logic deterministic to test.</p>



<h2 class="wp-block-heading">Retry missing or invalid input</h2>



<p class="wp-block-paragraph">The approval loop retries only when the user has not made a valid decision:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">for</span> (<span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">attempt</span> <span class="tok-operator">=</span> <span class="tok-number">1</span>; <span class="tok-variableName">attempt</span> <span class="tok-operator">&lt;=</span> <span class="tok-variableName">maxAttempts</span>; <span class="tok-variableName">attempt</span><span class="tok-operator">++</span>)</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">Console</span>.<span class="tok-variableName">Write</span>(</div><div class="cm-line">        <span class="tok-variableName">$</span><span class="tok-string">&quot;Approve tool {toolName}({arguments})? [y/n] &quot;</span> <span class="tok-operator">+</span></div><div class="cm-line">        <span class="tok-variableName">$</span><span class="tok-string">&quot;Attempt {attempt}/{maxAttempts}, &quot;</span> <span class="tok-operator">+</span></div><div class="cm-line">        <span class="tok-variableName">$</span><span class="tok-string">&quot;{timeout.TotalSeconds:0}s timeout: &quot;</span>);</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">input</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">readLineAsync</span>(<span class="tok-variableName">timeout</span>, <span class="tok-variableName">cancellationToken</span>);</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-variableName">input</span> <span class="tok-keyword">is</span> <span class="tok-atom">null</span>)</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">attempt</span> <span class="tok-operator">==</span> <span class="tok-variableName">maxAttempts</span></div><div class="cm-line">            <span class="tok-operator">?</span> <span class="tok-string">&quot;Timed out. Approval denied after the final attempt.&quot;</span></div><div class="cm-line">            : <span class="tok-string">&quot;Timed out. Retrying approval.&quot;</span>);</div><div class="cm-line">        <span class="tok-keyword">continue</span>;</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">input</span> <span class="tok-operator">=</span> <span class="tok-variableName">input</span>.<span class="tok-variableName">Trim</span>();</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-variableName">input</span>.<span class="tok-variableName">Equals</span>(<span class="tok-string">&quot;y&quot;</span>, <span class="tok-variableName">StringComparison</span>.<span class="tok-variableName">OrdinalIgnoreCase</span>) <span class="tok-operator">||</span></div><div class="cm-line">        <span class="tok-variableName">input</span>.<span class="tok-variableName">Equals</span>(<span class="tok-string">&quot;yes&quot;</span>, <span class="tok-variableName">StringComparison</span>.<span class="tok-variableName">OrdinalIgnoreCase</span>))</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ApprovalDecision</span>(</div><div class="cm-line">            <span class="tok-atom">true</span>,</div><div class="cm-line">            <span class="tok-variableName">$</span><span class="tok-string">&quot;Approved by console user on attempt {attempt}.&quot;</span>);</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-variableName">input</span>.<span class="tok-variableName">Equals</span>(<span class="tok-string">&quot;n&quot;</span>, <span class="tok-variableName">StringComparison</span>.<span class="tok-variableName">OrdinalIgnoreCase</span>) <span class="tok-operator">||</span></div><div class="cm-line">        <span class="tok-variableName">input</span>.<span class="tok-variableName">Equals</span>(<span class="tok-string">&quot;no&quot;</span>, <span class="tok-variableName">StringComparison</span>.<span class="tok-variableName">OrdinalIgnoreCase</span>))</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ApprovalDecision</span>(</div><div class="cm-line">            <span class="tok-atom">false</span>,</div><div class="cm-line">            <span class="tok-variableName">$</span><span class="tok-string">&quot;Denied by console user on attempt {attempt}.&quot;</span>);</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">attempt</span> <span class="tok-operator">==</span> <span class="tok-variableName">maxAttempts</span></div><div class="cm-line">        <span class="tok-operator">?</span> <span class="tok-string">&quot;Invalid response. Approval denied after the final attempt.&quot;</span></div><div class="cm-line">        : <span class="tok-string">&quot;Invalid response. Enter y or n; retrying approval.&quot;</span>);</div><div class="cm-line">}</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">return</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ApprovalDecision</span>(</div><div class="cm-line">    <span class="tok-atom">false</span>,</div><div class="cm-line">    <span class="tok-variableName">$</span><span class="tok-string">&quot;Denied automatically after {maxAttempts} unanswered or invalid attempts.&quot;</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">There is an important distinction here:</p>



<ul class="wp-block-list">
<li><strong>No response</strong> is not a denial yet, so the policy may ask again.</li>



<li><strong>Invalid input</strong> is not approval, so the policy may ask again.</li>



<li><strong>Explicit <code>n</code></strong> is a decision, so the policy stops immediately.</li>



<li><strong>Explicit <code>y</code></strong> is consent, so the policy stops immediately.</li>



<li><strong>No valid answer after the limit</strong> becomes an automatic denial.</li>
</ul>



<p class="wp-block-paragraph">Retries must never turn uncertainty into approval.</p>



<h2 class="wp-block-heading">Do not block forever on console input</h2>



<p class="wp-block-paragraph"><code>Console.ReadLine()</code>&nbsp;waits indefinitely and cannot provide the timeout behavior we need for an interactive console.</p>



<p class="wp-block-paragraph">The sample checks for available keyboard input until the deadline:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">input</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">StringBuilder</span>();</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">startedAt</span> <span class="tok-operator">=</span> <span class="tok-variableName">Stopwatch</span>.<span class="tok-variableName">GetTimestamp</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">while</span> (<span class="tok-variableName">Stopwatch</span>.<span class="tok-variableName">GetElapsedTime</span>(<span class="tok-variableName">startedAt</span>) <span class="tok-operator">&lt;</span> <span class="tok-variableName">timeout</span>)</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">cancellationToken</span>.<span class="tok-variableName">ThrowIfCancellationRequested</span>();</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-operator">!</span><span class="tok-variableName">Console</span>.<span class="tok-variableName">KeyAvailable</span>)</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">await</span> <span class="tok-typeName">Task</span>.<span class="tok-variableName">Delay</span>(<span class="tok-number">50</span>, <span class="tok-variableName">cancellationToken</span>);</div><div class="cm-line">        <span class="tok-keyword">continue</span>;</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">key</span> <span class="tok-operator">=</span> <span class="tok-variableName">Console</span>.<span class="tok-variableName">ReadKey</span>(<span class="tok-variableName">intercept</span>: <span class="tok-atom">true</span>);</div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-variableName">key</span>.<span class="tok-variableName">Key</span> <span class="tok-operator">==</span> <span class="tok-variableName">ConsoleKey</span>.<span class="tok-variableName">Enter</span>)</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>();</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">input</span>.<span class="tok-variableName">ToString</span>().<span class="tok-variableName">Trim</span>();</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-operator">!</span><span class="tok-typeName">char</span>.<span class="tok-variableName">IsControl</span>(<span class="tok-variableName">key</span>.<span class="tok-variableName">KeyChar</span>))</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">input</span>.<span class="tok-variableName">Append</span>(<span class="tok-variableName">key</span>.<span class="tok-variableName">KeyChar</span>);</div><div class="cm-line">        <span class="tok-variableName">Console</span>.<span class="tok-variableName">Write</span>(<span class="tok-variableName">key</span>.<span class="tok-variableName">KeyChar</span>);</div><div class="cm-line">    }</div><div class="cm-line">}</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>();</div><div class="cm-line"><span class="tok-keyword">return</span> <span class="tok-atom">null</span>;</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The complete implementation also supports redirected input and distinguishes an input timeout from caller cancellation. Cancellation, console I/O errors, and unavailable input are converted into denied decisions instead of allowing the tool to run.</p>



<h2 class="wp-block-heading">Make denial sticky for the prompt</h2>



<p class="wp-block-paragraph">Limiting keyboard attempts is not enough.</p>



<p class="wp-block-paragraph">After receiving a denial, the model could theoretically request the same tool again when the agent continues. A new request must not silently reopen the approval process during the same user prompt.</p>



<p class="wp-block-paragraph">The console runner records whether any approval has already been denied:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">approvalRound</span> <span class="tok-operator">=</span> <span class="tok-number">0</span>;</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">approvalDeniedForPrompt</span> <span class="tok-operator">=</span> <span class="tok-atom">false</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">while</span> (<span class="tok-atom">true</span>)</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">approvalRequests</span> <span class="tok-operator">=</span> <span class="tok-variableName">response</span>.<span class="tok-variableName">Messages</span></div><div class="cm-line">        .<span class="tok-variableName">SelectMany</span>(<span class="tok-variableName">message</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">message</span>.<span class="tok-variableName">Contents</span>)</div><div class="cm-line">        .<span class="tok-variableName">OfType</span><span class="tok-operator">&lt;</span><span class="tok-variableName">ToolApprovalRequestContent</span><span class="tok-operator">&gt;</span>()</div><div class="cm-line">        .<span class="tok-variableName">ToList</span>();</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-variableName">approvalRequests</span>.<span class="tok-variableName">Count</span> <span class="tok-operator">==</span> <span class="tok-number">0</span>)</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">return</span>;</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">approvalRound</span><span class="tok-operator">++</span>;</div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-variableName">approvalRound</span> <span class="tok-operator">&gt;</span> <span class="tok-variableName">MaxApprovalRoundsPerPrompt</span>)</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(</div><div class="cm-line">            <span class="tok-string">&quot;Approval denied: the agent exceeded the approval round limit &quot;</span> <span class="tok-operator">+</span></div><div class="cm-line">            <span class="tok-string">&quot;for this prompt.&quot;</span>);</div><div class="cm-line">        <span class="tok-keyword">return</span>;</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">approvalResponses</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">List</span><span class="tok-operator">&lt;</span><span class="tok-variableName">AIContent</span><span class="tok-operator">&gt;</span>();</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">foreach</span> (<span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">request</span> <span class="tok-keyword">in</span> <span class="tok-variableName">approvalRequests</span>)</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">functionCall</span> <span class="tok-operator">=</span> <span class="tok-variableName">request</span>.<span class="tok-variableName">ToolCall</span> <span class="tok-keyword">as</span> <span class="tok-variableName">FunctionCallContent</span>;</div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">toolName</span> <span class="tok-operator">=</span> <span class="tok-variableName">functionCall</span><span class="tok-operator">?</span>.<span class="tok-variableName">Name</span> <span class="tok-operator">??</span> <span class="tok-variableName">request</span>.<span class="tok-variableName">ToolCall</span>.<span class="tok-variableName">CallId</span>;</div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">arguments</span> <span class="tok-operator">=</span> <span class="tok-variableName">functionCall</span><span class="tok-operator">?</span>.<span class="tok-variableName">Arguments</span> <span class="tok-keyword">is</span> <span class="tok-atom">null</span></div><div class="cm-line">            <span class="tok-operator">?</span> <span class="tok-typeName">string</span>.<span class="tok-variableName">Empty</span></div><div class="cm-line">            : <span class="tok-typeName">string</span>.<span class="tok-variableName">Join</span>(</div><div class="cm-line">                <span class="tok-string">&quot;, &quot;</span>,</div><div class="cm-line">                <span class="tok-variableName">functionCall</span>.<span class="tok-variableName">Arguments</span>.<span class="tok-variableName">Select</span>(</div><div class="cm-line">                    <span class="tok-variableName">item</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">$</span><span class="tok-string">&quot;{item.Key}: {item.Value}&quot;</span>));</div><div class="cm-line"></div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">decision</span> <span class="tok-operator">=</span> <span class="tok-variableName">approvalDeniedForPrompt</span></div><div class="cm-line">            <span class="tok-operator">?</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ApprovalDecision</span>(</div><div class="cm-line">                <span class="tok-atom">false</span>,</div><div class="cm-line">                <span class="tok-string">&quot;Denied automatically because another approval was already &quot;</span> <span class="tok-operator">+</span></div><div class="cm-line">                <span class="tok-string">&quot;denied for this prompt.&quot;</span>)</div><div class="cm-line">            : <span class="tok-keyword">await</span> <span class="tok-variableName">approvalPolicy</span>.<span class="tok-variableName">RequestApprovalAsync</span>(<span class="tok-variableName">toolName</span>, <span class="tok-variableName">arguments</span>);</div><div class="cm-line"></div><div class="cm-line">        <span class="tok-variableName">approvalDeniedForPrompt</span> <span class="tok-operator">|=</span> <span class="tok-operator">!</span><span class="tok-variableName">decision</span>.<span class="tok-variableName">Approved</span>;</div><div class="cm-line">        <span class="tok-variableName">approvalResponses</span>.<span class="tok-variableName">Add</span>(</div><div class="cm-line">            <span class="tok-variableName">request</span>.<span class="tok-variableName">CreateResponse</span>(<span class="tok-variableName">decision</span>.<span class="tok-variableName">Approved</span>, <span class="tok-variableName">decision</span>.<span class="tok-variableName">Message</span>));</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">response</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">agent</span>.<span class="tok-variableName">RunAsync</span>(</div><div class="cm-line">        [<span class="tok-keyword">new</span> <span class="tok-variableName">ChatMessage</span>(<span class="tok-variableName">ChatRole</span>.<span class="tok-variableName">User</span>, <span class="tok-variableName">approvalResponses</span>)],</div><div class="cm-line">        <span class="tok-variableName">session</span>);</div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">This adds two separate limits:</p>



<ol class="wp-block-list">
<li>The human gets a bounded number of attempts to answer one approval request.</li>



<li>The agent gets a bounded number of approval rounds for one user prompt.</li>
</ol>



<p class="wp-block-paragraph">Once a request is denied, later approval requests in that prompt are denied automatically. The model cannot negotiate its way around the user&#8217;s answer.</p>



<h2 class="wp-block-heading">Demo paths</h2>



<p class="wp-block-paragraph">Run the sample:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet run --project `</div><div class="cm-line">  .\session-02\samples\22-approval-retries-timeouts\MafClaw.Sample22.csproj</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then use the same agent prompt for each path:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Buy 10 shares of MSFT.</div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">Approved</h3>



<p class="wp-block-paragraph">Answer before the deadline:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">y</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The tool executes and returns a simulated result.</p>



<h3 class="wp-block-heading">Explicitly denied</h3>



<p class="wp-block-paragraph">Answer:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">n</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The request is denied immediately. It does not consume the remaining attempts.</p>



<h3 class="wp-block-heading">Invalid input followed by approval</h3>



<p class="wp-block-paragraph">Answer:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">maybe</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The policy rejects the value and asks again. Answer&nbsp;<code>y</code>&nbsp;on the next attempt to approve.</p>



<h3 class="wp-block-heading">No response</h3>



<p class="wp-block-paragraph">Do not type anything.</p>



<p class="wp-block-paragraph">Each attempt expires after five seconds. After the fifth attempt, the request is denied automatically and the simulated trade does not execute.</p>



<h2 class="wp-block-heading">Why this matters outside the demo</h2>



<p class="wp-block-paragraph">Human approval is often presented as a yes/no dialog, but the operational behavior around that dialog is part of the security boundary.</p>



<p class="wp-block-paragraph">A real approval design should answer questions such as:</p>



<ul class="wp-block-list">
<li>How long is approval valid?</li>



<li>What happens when the approver is unavailable?</li>



<li>Can an expired request be retried?</li>



<li>How many retries are allowed?</li>



<li>Does an explicit denial remain final?</li>



<li>Can the agent immediately ask again?</li>



<li>What happens if multiple tools request approval together?</li>



<li>What audit data identifies the user, tool, arguments, decision, and time?</li>



<li>Does the approval authorize exactly one execution?</li>
</ul>



<p class="wp-block-paragraph">The sample is deliberately small, but it demonstrates the core rule: the host application owns these policies. The language model should not decide how long to wait, how many times to retry, or whether silence means approval.</p>



<h2 class="wp-block-heading">Try the complete sample</h2>



<p class="wp-block-paragraph">The complete source is available in the public MafClaw repository:</p>



<ul class="wp-block-list">
<li><a href="https://github.com/elbruno/mafclaw/tree/main/session-02/samples/22-approval-retries-timeouts" target="_blank" rel="noopener">Sample 22 &#8211; Approval retries and timeouts</a></li>



<li><a href="https://github.com/elbruno/mafclaw/tree/main/session-02/samples" target="_blank" rel="noopener">Session 2 samples</a></li>



<li><a href="https://aka.ms/mafclaw/repo" target="_blank" rel="noopener">MafClaw repository</a></li>
</ul>



<p class="wp-block-paragraph">And if you want to see the audience discussion that prompted this additional scenario, watch&nbsp;<a href="https://www.youtube.com/watch?v=V58coa0llUo" target="_blank" rel="noopener">Working With Your Data, Safely: Files, Approvals and Memory</a>&nbsp;on the .NET YouTube channel.</p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/09/10/what-happens-when-nobody-approves-the-agent/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">43724</post-id>
		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>
	</item>
		<item>
		<title>The deployment succeeded, but I deployed the wrong agent</title>
		<link>https://elbruno.com/2026/08/31/the-deployment-succeeded-but-i-deployed-the-wrong-agent/</link>
					<comments>https://elbruno.com/2026/08/31/the-deployment-succeeded-but-i-deployed-the-wrong-agent/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Mon, 31 Aug 2026 18:03:56 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Foundry Hosted Agents]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Microsoft Foundry Hosted Agents]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=43719</guid>

					<description><![CDATA[Hi! Ok so I spend the full morning with this, so let&#8217;s better write it down for Future Bruno: The deployment was green. The hosted agent was active. The Microsoft Foundry Playground could reach it. There were no authentication failures, container crashes, or model errors. There was just one problem: when I asked the C# [&#8230;]]]></description>
										<content:encoded><![CDATA[
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>
</blockquote>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">Ok so I spend the full morning with this, so let&#8217;s better write it down for Future Bruno:</p>



<p class="wp-block-paragraph">The deployment was green. The hosted agent was active. The Microsoft Foundry Playground could reach it. There were no authentication failures, container crashes, or model errors.</p>



<p class="wp-block-paragraph">There was just one problem: when I asked the C# agent, &#8220;Who are you?&#8221;, it answered:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">I&#8217;m ChatGPT, an AI assistant created by OpenAI.</p>
</blockquote>



<p class="wp-block-paragraph">That was not the behavior defined in the source code. The C# sample was explicitly instructed to identify itself as the C# hosted agent. The same problem appeared in the Go and C++ hosted agents.</p>



<p class="wp-block-paragraph">My first suspicion was stale build output or an aggressive container cache. The real cause was simpler and more useful:&nbsp;<strong>I had deployed from the wrong Git revision</strong>.</p>



<h2 class="wp-block-heading">The setup</h2>



<p class="wp-block-paragraph">You can find the working samples in the&nbsp;<a href="https://github.com/Azure-Samples/microsoft-foundry-hosted-agents">microsoft-foundry-hosted-agents repository</a>. For a focused walkthrough of the C# sample, see&nbsp;<a href="https://devblogs.microsoft.com/dotnet/from-dotnet-run-to-foundry-hosted-agent-in-3-lines-of-csharp/">From&nbsp;<code>dotnet run</code>&nbsp;to Foundry Hosted Agent in 3 Lines of C#</a>&nbsp;on the .NET Blog.</p>



<p class="wp-block-paragraph">The repository contains three Microsoft Foundry hosted-agent samples:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Sample</th><th class="has-text-align-left" data-align="left">Runtime</th><th class="has-text-align-left" data-align="left">Protocol</th><th class="has-text-align-left" data-align="left">Deployment style</th></tr></thead><tbody><tr><td>C#</td><td>.NET 10 and Microsoft Agent Framework</td><td>Responses</td><td>Direct code deployment</td></tr><tr><td>Go</td><td>Go 1.26 and Microsoft Agent Framework</td><td>Invocations</td><td>Container deployment</td></tr><tr><td>C++</td><td>C++20 and a repository-owned hosting adapter</td><td>Invocations</td><td>Container deployment</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Each agent has language-specific instructions. For example, the C# agent is configured with:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-variableName">instructions</span>: <span class="tok-string">&quot;You are the C# hosted agent sample, running on .NET 10 with Microsoft Agent Framework. &quot;</span> <span class="tok-operator">+</span></div><div class="cm-line">              <span class="tok-string">&quot;When greeted or asked who you are, identify yourself as the C# hosted agent. Keep your answers brief.&quot;</span></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The Go and C++ samples have equivalent instructions for their own runtimes.</p>



<p class="wp-block-paragraph">Despite that, all three deployed agents returned the generic model identity.</p>



<h2 class="wp-block-heading">Why the obvious evidence was misleading</h2>



<p class="wp-block-paragraph">Several facts initially suggested a packaging problem:</p>



<ul class="wp-block-list">
<li>The source files contained the correct language-specific instructions.</li>



<li>All deployed agent versions were active.</li>



<li>Each endpoint returned a successful model response.</li>



<li>The issue reproduced in a completely new session.</li>



<li>For the C# sample, inspecting the deployed build output revealed the older prompt: <code>You are a friendly assistant.</code></li>
</ul>



<p class="wp-block-paragraph">That last detail looked like proof of a stale compiler artifact. It was proof that the deployed artifact was old, but not proof that the compiler or deployment service had incorrectly cached it.</p>



<p class="wp-block-paragraph">The Git reflog and timestamps completed the picture.</p>



<h2 class="wp-block-heading">The timeline that exposed the cause</h2>



<p class="wp-block-paragraph">The original versions were deployed between 11:30 and 11:54:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Time</th><th class="has-text-align-left" data-align="left">Event</th></tr></thead><tbody><tr><td>11:30</td><td>Go hosted agent version 1 deployed</td></tr><tr><td>11:52</td><td>C++ hosted agent version 1 deployed</td></tr><tr><td>11:54</td><td>C# hosted agent version 2 deployed</td></tr><tr><td>12:04</td><td>The workspace switched from a feature branch to&nbsp;<code>main</code></td></tr><tr><td>12:04</td><td><code>main</code>&nbsp;was fast-forwarded from&nbsp;<code>origin/main</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The language-specific instructions arrived in the working tree with that 12:04 branch switch and pull. The deployments had already happened.</p>



<p class="wp-block-paragraph">The local source looked correct during the investigation because I was looking at&nbsp;<code>main</code>. Microsoft Foundry was correctly running the artifacts produced earlier from the feature branch.</p>



<p class="wp-block-paragraph">There was no mysterious cloud cache. I had successfully deployed exactly the wrong code.</p>



<h2 class="wp-block-heading">Confirming the diagnosis</h2>



<p class="wp-block-paragraph">Before changing anything, I reproduced the behavior outside the Playground. This ruled out old browser conversation history.</p>



<p class="wp-block-paragraph">For the C# Responses agent:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">azd</span> <span class="tok-variableName">ai</span> <span class="tok-variableName">agent</span> <span class="tok-variableName">invoke</span> <span class="tok-operator">--</span><span class="tok-variableName">new-session</span> <span class="tok-operator">--</span><span class="tok-variableName">new-conversation</span> <span class="tok-string">&quot;who are you?&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">For the Go and C++ Invocations agents:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">azd</span> <span class="tok-variableName">ai</span> <span class="tok-variableName">agent</span> <span class="tok-variableName">invoke</span> <span class="tok-operator">--</span><span class="tok-variableName">new-session</span> <span class="tok-string">&quot;who are you?&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">All three returned the generic identity.</p>



<p class="wp-block-paragraph">Next, I checked the deployed version:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">azd</span> <span class="tok-variableName">ai</span> <span class="tok-variableName">agent</span> <span class="tok-variableName">show</span> <span class="tok-operator">--</span><span class="tok-variableName">output</span> <span class="tok-variableName">json</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then I compared the repository state:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">git</span> <span class="tok-variableName">branch</span> <span class="tok-operator">--</span><span class="tok-variableName">show-current</span></div><div class="cm-line"><span class="tok-variableName">git</span> <span class="tok-variableName">status</span> <span class="tok-operator">--</span><span class="tok-variableName">short</span></div><div class="cm-line"><span class="tok-variableName">git</span> <span class="tok-variableName">rev-parse</span> <span class="tok-variableName">HEAD</span></div><div class="cm-line"><span class="tok-variableName">git</span> <span class="tok-variableName">rev-parse</span> <span class="tok-variableName">origin</span><span class="tok-operator">/</span><span class="tok-variableName">main</span></div><div class="cm-line"><span class="tok-variableName">git</span> <span class="tok-variableName">reflog</span> <span class="tok-operator">--</span><span class="tok-variableName">date</span><span class="tok-operator">=</span><span class="tok-variableName">iso</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The reflog was the decisive evidence. It showed that the deployments preceded the switch to the revision containing the new instructions.</p>



<h2 class="wp-block-heading">The fix</h2>



<p class="wp-block-paragraph">I verified that:</p>



<ol class="wp-block-list">
<li>the active branch was <code>main</code>;</li>



<li><code>HEAD</code> matched <code>origin/main</code>;</li>



<li>the source contained the intended instructions; and</li>



<li>the local builds contained the intended prompt.</li>
</ol>



<p class="wp-block-paragraph">I then redeployed all three samples. Microsoft Foundry created new immutable agent versions:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Sample</th><th class="has-text-align-right" data-align="right">Corrected version</th><th class="has-text-align-left" data-align="left">Verified response</th></tr></thead><tbody><tr><td>C#</td><td class="has-text-align-right" data-align="right">3</td><td>&#8220;I am the C# hosted agent sample&#8230;&#8221;</td></tr><tr><td>Go</td><td class="has-text-align-right" data-align="right">2</td><td>&#8220;I am the Go hosted agent.&#8221;</td></tr><tr><td>C++</td><td class="has-text-align-right" data-align="right">2</td><td>&#8220;I am the C++ hosted agent.&#8221;</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Each corrected deployment was tested with a new session. The C# Responses agent also used a new conversation to guarantee that no previous conversation history influenced the result.</p>



<h2 class="wp-block-heading">A safer deployment gate</h2>



<p class="wp-block-paragraph">The key lesson is that a successful build and a successful deployment answer different questions:</p>



<ul class="wp-block-list">
<li><strong>Build succeeded:</strong> this revision can be compiled.</li>



<li><strong>Deployment succeeded:</strong> these packaged bytes can run.</li>



<li><strong>Behavioral smoke test succeeded:</strong> the active deployment behaves like the revision I intended to release.</li>
</ul>



<p class="wp-block-paragraph">All three checks are necessary.</p>



<h3 class="wp-block-heading">1. Verify the Git revision</h3>



<p class="wp-block-paragraph">Run this before packaging or deploying:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">git</span> <span class="tok-variableName">fetch</span> <span class="tok-variableName">origin</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">$branch</span> <span class="tok-operator">=</span> <span class="tok-variableName">git</span> <span class="tok-variableName">branch</span> <span class="tok-operator">--</span><span class="tok-variableName">show-current</span></div><div class="cm-line"><span class="tok-variableName">$head</span> <span class="tok-operator">=</span> <span class="tok-variableName">git</span> <span class="tok-variableName">rev-parse</span> <span class="tok-variableName">HEAD</span></div><div class="cm-line"><span class="tok-variableName">$remote</span> <span class="tok-operator">=</span> <span class="tok-variableName">git</span> <span class="tok-variableName">rev-parse</span> <span class="tok-variableName">origin</span><span class="tok-operator">/</span><span class="tok-variableName">main</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Write-Host</span> <span class="tok-string">&quot;Branch: </span><span class="tok-variableName">$branch</span><span class="tok-string">&quot;</span></div><div class="cm-line"><span class="tok-variableName">Write-Host</span> <span class="tok-string">&quot;HEAD:   </span><span class="tok-variableName">$head</span><span class="tok-string">&quot;</span></div><div class="cm-line"><span class="tok-variableName">Write-Host</span> <span class="tok-string">&quot;Remote: </span><span class="tok-variableName">$remote</span><span class="tok-string">&quot;</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">git</span> <span class="tok-variableName">status</span> <span class="tok-operator">--</span><span class="tok-variableName">short</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">if</span> <span class="tok-punctuation">(</span><span class="tok-variableName">$branch</span> <span class="tok-operator">-ne</span> <span class="tok-string">&quot;main&quot;</span><span class="tok-punctuation">)</span> <span class="tok-punctuation">{</span></div><div class="cm-line">    <span class="tok-keyword">throw</span> <span class="tok-string">&quot;Deployment must run from main.&quot;</span></div><div class="cm-line"><span class="tok-punctuation">}</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">if</span> <span class="tok-punctuation">(</span><span class="tok-variableName">$head</span> <span class="tok-operator">-ne</span> <span class="tok-variableName">$remote</span><span class="tok-punctuation">)</span> <span class="tok-punctuation">{</span></div><div class="cm-line">    <span class="tok-keyword">throw</span> <span class="tok-string">&quot;Local HEAD does not match origin/main.&quot;</span></div><div class="cm-line"><span class="tok-punctuation">}</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Whether deployments must come from&nbsp;<code>main</code>, a release branch, or a tag is a team decision. The important part is to make the rule explicit and fail before deployment when it is not satisfied.</p>



<p class="wp-block-paragraph">For production releases, a tag or immutable commit SHA is safer than relying only on a branch name:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">$expectedCommit</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;&lt;approved-commit-sha&gt;&quot;</span></div><div class="cm-line"><span class="tok-variableName">$actualCommit</span> <span class="tok-operator">=</span> <span class="tok-variableName">git</span> <span class="tok-variableName">rev-parse</span> <span class="tok-variableName">HEAD</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">if</span> <span class="tok-punctuation">(</span><span class="tok-variableName">$actualCommit</span> <span class="tok-operator">-ne</span> <span class="tok-variableName">$expectedCommit</span><span class="tok-punctuation">)</span> <span class="tok-punctuation">{</span></div><div class="cm-line">    <span class="tok-keyword">throw</span> <span class="tok-string">&quot;Workspace is not at the approved release commit.&quot;</span></div><div class="cm-line"><span class="tok-punctuation">}</span></div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">2. Build and test from that revision</h3>



<p class="wp-block-paragraph">Use the repository&#8217;s normal validation commands before deployment:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">build</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">MAF-Agents-Samples</span><span class="tok-punctuation">.</span><span class="tok-variableName">slnx</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Push-Location</span> <span class="tok-punctuation">.\</span><span class="tok-number">04</span><span class="tok-operator">-</span><span class="tok-variableName">MAF-Agent-GO-Hosted</span></div><div class="cm-line"><span class="tok-variableName">go</span> <span class="tok-variableName">test</span> <span class="tok-punctuation">.</span><span class="tok-operator">/..</span><span class="tok-punctuation">.</span></div><div class="cm-line"><span class="tok-variableName">Pop-Location</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Push-Location</span> <span class="tok-punctuation">.\</span><span class="tok-number">06</span><span class="tok-operator">-</span><span class="tok-variableName">Foundry-Agent-CPP-Hosted</span></div><div class="cm-line"><span class="tok-variableName">cmake</span> <span class="tok-operator">--</span><span class="tok-variableName">preset</span> <span class="tok-variableName">debug</span></div><div class="cm-line"><span class="tok-variableName">cmake</span> <span class="tok-operator">--</span><span class="tok-variableName">build</span> <span class="tok-operator">--</span><span class="tok-variableName">preset</span> <span class="tok-variableName">debug</span></div><div class="cm-line"><span class="tok-variableName">ctest</span> <span class="tok-operator">--</span><span class="tok-variableName">preset</span> <span class="tok-variableName">debug</span></div><div class="cm-line"><span class="tok-variableName">Pop-Location</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">This catches source and dependency problems, but it still does not prove that the intended revision reached Microsoft Foundry.</p>



<h3 class="wp-block-heading">3. Record the commit with the deployment</h3>



<p class="wp-block-paragraph">Capture the SHA in deployment logs:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">$commit</span> <span class="tok-operator">=</span> <span class="tok-variableName">git</span> <span class="tok-variableName">rev-parse</span> <span class="tok-variableName">HEAD</span></div><div class="cm-line"><span class="tok-variableName">Write-Host</span> <span class="tok-string">&quot;Deploying commit </span><span class="tok-variableName">$commit</span><span class="tok-string">&quot;</span></div><div class="cm-line"><span class="tok-variableName">azd</span> <span class="tok-variableName">deploy</span> <span class="tok-operator">--</span><span class="tok-variableName">no-prompt</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">In a CI/CD pipeline, preserve this value as release metadata or an artifact. Being able to answer &#8220;Which commit produced agent version 7?&#8221; turns a long investigation into a quick comparison.</p>



<h3 class="wp-block-heading">4. Verify the active immutable version</h3>



<p class="wp-block-paragraph">After deployment:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">azd</span> <span class="tok-variableName">ai</span> <span class="tok-variableName">agent</span> <span class="tok-variableName">show</span> <span class="tok-operator">--</span><span class="tok-variableName">output</span> <span class="tok-variableName">json</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Confirm that:</p>



<ul class="wp-block-list">
<li>the new version is active;</li>



<li>the protocol is correct;</li>



<li>the model deployment and environment variables are correct; and</li>



<li>the endpoint points to the expected agent.</li>
</ul>



<p class="wp-block-paragraph">Do not assume that a completed&nbsp;<code>azd deploy</code>&nbsp;means the endpoint has already switched to the version you intended.</p>



<h3 class="wp-block-heading">5. Test a distinguishing behavior</h3>



<p class="wp-block-paragraph"><code>Hello</code>&nbsp;is a weak smoke test. Almost any functioning model can answer it.</p>



<p class="wp-block-paragraph">Choose a prompt whose expected response distinguishes the new release from the old one. In this case, identity was ideal:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">azd</span> <span class="tok-variableName">ai</span> <span class="tok-variableName">agent</span> <span class="tok-variableName">invoke</span> <span class="tok-operator">--</span><span class="tok-variableName">new-session</span> <span class="tok-string">&quot;who are you?&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">For a Responses agent, reset both forms of state:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">azd</span> <span class="tok-variableName">ai</span> <span class="tok-variableName">agent</span> <span class="tok-variableName">invoke</span> <span class="tok-operator">--</span><span class="tok-variableName">new-session</span> <span class="tok-operator">--</span><span class="tok-variableName">new-conversation</span> <span class="tok-string">&quot;who are you?&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Other useful distinguishing checks include:</p>



<ul class="wp-block-list">
<li>a newly added tool;</li>



<li>a changed refusal or safety rule;</li>



<li>a version-specific response marker;</li>



<li>a newly supported data source; or</li>



<li>a corrected edge case.</li>
</ul>



<p class="wp-block-paragraph">The smoke test should fail when the previous release is still active.</p>



<h3 class="wp-block-heading">6. Evaluate the behavior you actually care about</h3>



<p class="wp-block-paragraph">The original evaluation descriptions called these agents friendly, general-purpose assistants. That criterion would pass for both the incorrect and corrected deployments.</p>



<p class="wp-block-paragraph">The evaluation criteria now explicitly require each agent to identify itself as the C#, Go, or C++ hosted sample. This turns identity from an informal expectation into a regression check.</p>



<p class="wp-block-paragraph">Evaluations should describe the behavior that makes the release distinct—not only generic qualities such as helpfulness, relevance, and brevity.</p>



<h2 class="wp-block-heading">CI/CD is the long-term solution</h2>



<p class="wp-block-paragraph">Manual deployment from a developer workstation makes the active branch, uncommitted changes, local build outputs, and shell environment part of the release process.</p>



<p class="wp-block-paragraph">A CI/CD deployment should instead:</p>



<ol class="wp-block-list">
<li>start from an explicit commit or signed tag;</li>



<li>require a clean checkout;</li>



<li>run builds and tests;</li>



<li>log the commit SHA;</li>



<li>deploy through <code>azd</code>;</li>



<li>wait for the new immutable agent version to become active;</li>



<li>invoke a release-specific smoke test in fresh state; and</li>



<li>run a small regression evaluation suite.</li>
</ol>



<p class="wp-block-paragraph">That process does not merely automate deployment. It makes the deployed source traceable and reproducible.</p>



<h2 class="wp-block-heading">What I would check next time</h2>



<p class="wp-block-paragraph">If deployed behavior does not match local source, I would investigate in this order:</p>



<ol class="wp-block-list">
<li>Reproduce with a new session and, for Responses, a new conversation.</li>



<li>Check the active agent version.</li>



<li>Print the current branch and commit SHA.</li>



<li>Compare <code>HEAD</code> with the approved release SHA.</li>



<li>Check <code>git status</code> for uncommitted changes.</li>



<li>Inspect the packaged artifact or container image.</li>



<li>Only then investigate build and container caching.</li>
</ol>



<p class="wp-block-paragraph">This order starts with the cheapest and most common explanations. It also separates three different sources of stale behavior:</p>



<ul class="wp-block-list">
<li>stale conversation state;</li>



<li>stale deployed version; and</li>



<li>stale or unintended source revision.</li>
</ul>



<h2 class="wp-block-heading">Final takeaway</h2>



<p class="wp-block-paragraph">The cloud platform did exactly what I asked it to do. The mistake was assuming that the source visible in my editor was the source I had deployed ten minutes earlier.</p>



<p class="wp-block-paragraph">The most important deployment question is not:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Did the deployment succeed?</p>
</blockquote>



<p class="wp-block-paragraph">It is:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Can I prove which revision is active, and can I prove that it behaves like that revision?</p>
</blockquote>



<p class="wp-block-paragraph">An immutable version number, a recorded Git SHA, and one distinguishing smoke test provide that proof.</p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/08/31/the-deployment-succeeded-but-i-deployed-the-wrong-agent/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">43719</post-id>
		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>
	</item>
		<item>
		<title>Local transcript cleanup in .NET with s1-mini</title>
		<link>https://elbruno.com/2026/08/21/local-transcript-cleanup-in-net-with-s1-mini/</link>
					<comments>https://elbruno.com/2026/08/21/local-transcript-cleanup-in-net-with-s1-mini/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Fri, 21 Aug 2026 16:24:55 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Hugging Face]]></category>
		<category><![CDATA[ONNX]]></category>
		<category><![CDATA[S1Mini]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=43714</guid>

					<description><![CDATA[Hi 👋 My favorite demo for this library is intentionally small: Raw ASR transcript Cleaned by s1-mini you don't have any any any any change at all? You don't have any change at all. That&#160;any any any any&#160;collapse is the interesting part. This is more than deleting a list of filler words. It is transcript [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.S1Mini/blob/main/docs/blogs/26-08-20-intro.md#local-transcript-cleanup-in-net-with-s1-mini"></a></p>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f44b.png" alt="👋" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">My favorite demo for this library is intentionally small:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Raw ASR transcript</th><th>Cleaned by s1-mini</th></tr></thead><tbody><tr><td><code>you don't have any any any any change at all?</code></td><td><code>You don't have any change at all.</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">That&nbsp;<code>any any any any</code>&nbsp;collapse is the interesting part. This is more than deleting a list of filler words. It is transcript normalization: turning the rough text emitted by speech-to-text into something a human would actually write.</p>



<p class="wp-block-paragraph"><a href="https://huggingface.co/superwhisper/s1-mini"><code>superwhisper/s1-mini</code></a>&nbsp;is a 0.6B-parameter Qwen3 fine-tune with one job:&nbsp;<strong>ASR transcript normalization</strong>. It is not a chat model. It is not a speech model. It does not listen to audio. It takes an existing raw speech-to-text transcript and rewrites it as clean written text.</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.S1Mini"><code>ElBruno.S1Mini</code></a>&nbsp;packages that workflow for .NET developers. It downloads an ONNX conversion from Hugging Face on first use, runs it locally with ONNX Runtime GenAI, and exposes a small C# API for normalizing transcripts. Nothing leaves the machine.</p>



<p class="wp-block-paragraph">The current NuGet package is&nbsp;<a href="https://www.nuget.org/packages/ElBruno.S1Mini"><code>ElBruno.S1Mini</code>&nbsp;0.1.1</a>. It targets&nbsp;<code>net8.0</code>&nbsp;and&nbsp;<code>net10.0</code>, and the C# code is MIT licensed.</p>



<h2 class="wp-block-heading">The basic C# sample<a href="https://github.com/elbruno/ElBruno.S1Mini/blob/main/docs/blogs/26-08-20-intro.md#the-basic-c-sample"></a></h2>



<p class="wp-block-paragraph">Install the package:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet add package ElBruno.S1Mini</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then normalize a transcript:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">S1Mini</span>.<span class="tok-variableName">Normalization</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">normalizer</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">TranscriptNormalizer</span>.<span class="tok-variableName">CreateAsync</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">cleaned</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">normalizer</span>.<span class="tok-variableName">NormalizeAsync</span>(</div><div class="cm-line">    <span class="tok-string">&quot;you don&apos;t have any any any any change at all?&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">cleaned</span>);</div><div class="cm-line"><span class="tok-comment">// You don&apos;t have any change at all.</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">On first use, the library downloads&nbsp;<code>elbruno/s1-mini-onnx</code>&nbsp;from Hugging Face into&nbsp;<code>%LOCALAPPDATA%/ElBruno/S1Mini/models</code>. The default model subfolder is&nbsp;<code>int4</code>, about 390 MB on disk, and subsequent runs use the cached copy.</p>



<p class="wp-block-paragraph">The main API is deliberately small:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">S1Mini</span>.<span class="tok-variableName">Normalization</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">normalizer</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">TranscriptNormalizer</span>.<span class="tok-variableName">CreateAsync</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">cleaned</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">normalizer</span>.<span class="tok-variableName">NormalizeAsync</span>(</div><div class="cm-line">    <span class="tok-string">&quot;so um i need to like send the the report by uh friday no wait make that thursday&quot;</span>,</div><div class="cm-line">    <span class="tok-keyword">new</span> <span class="tok-variableName">TranscriptNormalizerOptions</span></div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">Styling</span> <span class="tok-operator">=</span> <span class="tok-variableName">TranscriptStyling</span>.<span class="tok-variableName">SemiFormal</span>,</div><div class="cm-line">        <span class="tok-variableName">Structure</span> <span class="tok-operator">=</span> <span class="tok-variableName">TranscriptStructure</span>.<span class="tok-variableName">Prose</span>,</div><div class="cm-line">        <span class="tok-variableName">Context</span> <span class="tok-operator">=</span> <span class="tok-variableName">TranscriptContext</span>.<span class="tok-variableName">General</span>,</div><div class="cm-line">        <span class="tok-variableName">MaxTokens</span> <span class="tok-operator">=</span> <span class="tok-number">1024</span>,</div><div class="cm-line">    });</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">cleaned</span>);</div><div class="cm-line"><span class="tok-comment">// So I need to send the report by Thursday.</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">You can also configure the local model cache:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">S1Mini</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">S1Mini</span>.<span class="tok-variableName">Normalization</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">s1Options</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">S1MiniOptions</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">CacheDirectory</span> <span class="tok-operator">=</span> <span class="tok-string">@&quot;C:\models&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">EnsureModelDownloaded</span> <span class="tok-operator">=</span> <span class="tok-atom">true</span>,</div><div class="cm-line">};</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">normalizer</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">TranscriptNormalizer</span>.<span class="tok-variableName">CreateAsync</span>(<span class="tok-variableName">s1Options</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Or register it with dependency injection:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">S1Mini</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">builder</span>.<span class="tok-variableName">Services</span>.<span class="tok-variableName">AddTranscriptNormalizer</span>(<span class="tok-variableName">options</span> <span class="tok-operator">=&gt;</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">options</span>.<span class="tok-variableName">CacheDirectory</span> <span class="tok-operator">=</span> <span class="tok-string">@&quot;C:\models&quot;</span>;</div><div class="cm-line">});</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The package also exposes&nbsp;<code>S1MiniClient</code>, an&nbsp;<code>IChatClient</code>&nbsp;implementation for&nbsp;<code>Microsoft.Extensions.AI</code>, but the warning still matters: s1-mini is not a general chat model.&nbsp;<code>TranscriptNormalizer</code>&nbsp;is the safer layer because it builds the prompt shape the model expects.</p>



<h2 class="wp-block-heading">What the model actually cleaned today<a href="https://github.com/elbruno/ElBruno.S1Mini/blob/main/docs/blogs/26-08-20-intro.md#what-the-model-actually-cleaned-today"></a></h2>



<p class="wp-block-paragraph">These outputs were measured with the library today using greedy decoding and default options:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Raw transcript</th><th>s1-mini output</th></tr></thead><tbody><tr><td><code>you don't have any any any any change at all?</code></td><td><code>You don't have any change at all.</code></td></tr><tr><td><code>so um i need to like send the the report by uh friday no wait make that thursday</code></td><td><code>So I need to send the report by Thursday.</code></td></tr><tr><td><code>and then we we we need to to look at the the numbers</code></td><td><code>And then we need to look at the numbers.</code></td></tr><tr><td><code>i think we should uh go with with option b i mean option c</code></td><td><code>I think we should go with option C.</code></td></tr><tr><td><code>the the total was like twenty five dollars and uh fifty cents</code></td><td><code>The total was like $25.50.</code></td></tr><tr><td><code>lets meet at uh three thirty on on tuesday the the tenth</code></td><td><code>Let's meet at 3:30 on Tuesday the 10th.</code></td></tr></tbody></table></figure>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.S1Mini/blob/main/docs/blogs/images/before-after.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.S1Mini/raw/main/docs/blogs/images/before-after.png" alt="Before and after transcript cleanup examples" /></a></figure>



<p class="wp-block-paragraph">The examples show several useful behaviors in a compact set:</p>



<ul class="wp-block-list">
<li>filler removal: <code>um</code>, <code>uh</code></li>



<li>stutter and repetition collapse: <code>we we we</code>, <code>the the</code>, <code>any any any any</code></li>



<li>self-correction resolution: <code>option b i mean option c</code> becomes <code>option C</code></li>



<li>spoken-to-written numerals and currency: <code>twenty five dollars and fifty cents</code> becomes <code>$25.50</code></li>



<li>date and time formatting: <code>three thirty on tuesday the tenth</code> becomes <code>3:30 on Tuesday the 10th</code></li>
</ul>



<p class="wp-block-paragraph">One honest caveat is visible in the currency example: the model kept the colloquial&nbsp;<code>like</code>&nbsp;in&nbsp;<code>The total was like $25.50.</code>&nbsp;That is a good reminder of the boundary. This is a normalizer, not a rewriter. It cleans speech artifacts, punctuation, casing, numbers, dates, and corrections. It does not always turn casual phrasing into formal prose.</p>



<h2 class="wp-block-heading">The live microphone sample<a href="https://github.com/elbruno/ElBruno.S1Mini/blob/main/docs/blogs/26-08-20-intro.md#the-live-microphone-sample"></a></h2>



<p class="wp-block-paragraph">The repository includes a Windows-only sample at&nbsp;<code>src/samples/LiveMicTranscription</code>. Its pipeline is:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">microphone (NAudio, 16 kHz mono)</div><div class="cm-line">  -&gt; Silero VAD v5 (ElBruno.Realtime.SileroVad, ~2 MB)</div><div class="cm-line">  -&gt; Whisper (ElBruno.Whisper)</div><div class="cm-line">  -&gt; s1-mini (ElBruno.S1Mini)</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The console UI is built with&nbsp;<a href="https://spectreconsole.net/">Spectre.Console</a>. The sample supports live microphone capture, but it also supports repeatable runs:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">run</span> <span class="tok-operator">--</span><span class="tok-variableName">project</span> <span class="tok-variableName">src</span><span class="tok-operator">/</span><span class="tok-variableName">samples</span><span class="tok-operator">/</span><span class="tok-variableName">LiveMicTranscription</span> <span class="tok-operator">--</span> <span class="tok-operator">--</span><span class="tok-variableName">save-audio</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">run</span> <span class="tok-operator">--</span><span class="tok-variableName">project</span> <span class="tok-variableName">src</span><span class="tok-operator">/</span><span class="tok-variableName">samples</span><span class="tok-operator">/</span><span class="tok-variableName">LiveMicTranscription</span> <span class="tok-operator">--</span> <span class="tok-operator">--</span><span class="tok-variableName">wav</span> <span class="tok-variableName">recordings</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph"><code>--save-audio</code>&nbsp;records detected utterances.&nbsp;<code>--wav &lt;file|folder&gt;</code>&nbsp;replays a saved file or folder through the identical pipeline. That matters because live microphone testing is otherwise not repeatable. Every take is a different performance.</p>



<p class="wp-block-paragraph"><em>Caption: The live microphone sample loads Whisper, Silero VAD, and s1-mini locally before listening.</em></p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.S1Mini/blob/main/docs/blogs/images/pipeline.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.S1Mini/raw/main/docs/blogs/images/pipeline.png" alt="Pipeline diagram for microphone to Silero VAD to Whisper to s1-mini" /></a></figure>



<p class="wp-block-paragraph">The pipeline shape is not decorative. It came from debugging a real failure mode.</p>



<h3 class="wp-block-heading">The filler-word paradox<a href="https://github.com/elbruno/ElBruno.S1Mini/blob/main/docs/blogs/26-08-20-intro.md#the-filler-word-paradox"></a></h3>



<p class="wp-block-paragraph">The whole point of s1-mini is removing fillers like&nbsp;<code>um</code>&nbsp;and&nbsp;<code>uh</code>. The first version of the sample used a simple energy-threshold voice activity detector. That sounds reasonable until you remember what fillers are acoustically: low-energy sounds near the noise floor.</p>



<p class="wp-block-paragraph">The threshold gate removed the exact words the model was supposed to clean. Whisper never received them. s1-mini then had nothing to remove, and the model looked broken.</p>



<p class="wp-block-paragraph">The fix was switching to a neural VAD:&nbsp;<strong>Silero VAD v5</strong>&nbsp;via&nbsp;<code>ElBruno.Realtime.SileroVad</code>. Silero is small, around 2 MB, but it classifies speech instead of only measuring loudness. Quiet fillers survive long enough to reach Whisper.</p>



<p class="wp-block-paragraph">There was a second, subtler bug. Silero returns multiple speech segments. If you concatenate only those segments, you delete the quiet audio between them. That mangles prosody and can make Whisper lose the very hesitation you wanted to preserve.</p>



<p class="wp-block-paragraph">The correct approach is to use Silero&#8217;s timestamps to cut one contiguous slice: first segment start to last segment end, with about 400 ms of padding. The same spoken sentence produced this A/B result:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Strategy</th><th>Audio captured</th><th>Whisper output</th></tr></thead><tbody><tr><td>Energy threshold</td><td>3.0 s</td><td><code>So, um, hello.</code>&nbsp;(truncated at the first pause)</td></tr><tr><td>Silero, segments concatenated</td><td>8.9 s</td><td>fillers lost in the seams</td></tr><tr><td>Silero + contiguous slice</td><td>12.6 s</td><td>word-for-word exact, all 3 fillers preserved</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><em>Caption: The useful view is side-by-side: raw ASR transcript first, normalized text second.</em></p>



<p class="wp-block-paragraph">A common explanation is that Whisper strips disfluencies because it was trained on subtitles. That was tested here and was false for this setup. Whisper Tiny and Base both retained 4/4 fillers verbatim. Larger Whisper models do tidy more aggressively, so the sample defaults matter when you are testing cleanup behavior.</p>



<h2 class="wp-block-heading">Bonus: the ONNX migration and how s1-mini works internally<a href="https://github.com/elbruno/ElBruno.S1Mini/blob/main/docs/blogs/26-08-20-intro.md#bonus-the-onnx-migration-and-how-s1-mini-works-internally"></a></h2>



<p class="wp-block-paragraph">The ONNX model used by the package is&nbsp;<a href="https://huggingface.co/elbruno/s1-mini-onnx"><code>elbruno/s1-mini-onnx</code></a>, an&nbsp;<strong>explicitly unofficial, unaffiliated, non-endorsed</strong>&nbsp;derivative of&nbsp;<a href="https://huggingface.co/superwhisper/s1-mini"><code>superwhisper/s1-mini</code></a>. The upstream model is Apache-2.0 with a naming clause. The C# code in this repository is MIT. The downloaded weights remain under the upstream Apache-2.0 license.</p>



<p class="wp-block-paragraph">The conversion exists so .NET apps can run s1-mini locally through&nbsp;<code>Microsoft.ML.OnnxRuntimeGenAI</code>&nbsp;0.15.1. The package uses INT4 by default, and that is the variant to use today.</p>



<p class="wp-block-paragraph">The Hugging Face repository also publishes an&nbsp;<code>fp16/</code>&nbsp;folder, but that variant is broken on CPU with&nbsp;<code>onnxruntime-genai</code>&nbsp;0.15.1. Every prompt fails with a shape mismatch in the GQA&nbsp;<code>repeat_kv</code>&nbsp;Reshape node. This is an ONNX Runtime builder/graph-optimizer bug for FP16 + GQA on the CPU execution provider, not something specific to this conversion.&nbsp;<code>S1MiniOptions.ModelSubPath</code>&nbsp;defaults to&nbsp;<code>int4</code>&nbsp;for exactly this reason.</p>



<p class="wp-block-paragraph">There is another runtime edge case worth documenting:&nbsp;<code>temperature=0</code>. The model should run with greedy decoding. Callers should be able to pass&nbsp;<code>Temperature = 0f</code>. But&nbsp;<code>onnxruntime-genai</code>&#8216;s native runtime crashes with an integer divide-by-zero if a literal&nbsp;<code>temperature=0.0</code>&nbsp;is passed into native search options, even when&nbsp;<code>do_sample=False</code>.</p>



<p class="wp-block-paragraph"><code>ElBruno.S1Mini</code>&nbsp;guards that at the runtime layer.&nbsp;<code>Temperature &lt;= 0</code>&nbsp;is treated as the library-wide greedy contract, and the native&nbsp;<code>temperature</code>&nbsp;option is omitted. That keeps greedy decoding safe without changing it into sampling.</p>



<p class="wp-block-paragraph">Internally,&nbsp;<code>TranscriptNormalizer</code>&nbsp;sends the exact system prompt expected by the model:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-css"><div class="cm-line"><span class="tok-typeName">You</span> <span class="tok-typeName">are</span> <span class="tok-typeName">a</span> <span class="tok-typeName">text</span> <span class="tok-typeName">normalizer</span> <span class="tok-typeName">for</span> <span class="tok-typeName">speech-to-text</span> <span class="tok-typeName">transcripts</span>. <span class="tok-className">The</span> <span class="tok-typeName">input</span> <span class="tok-typeName">begins</span> <span class="tok-typeName">with</span> <span class="tok-typeName">a</span> <span class="tok-typeName">control</span> <span class="tok-typeName">line</span></div><div class="cm-line"><span class="tok-typeName">specifying</span> <span class="tok-typeName">the</span> <span class="tok-typeName">styling</span><span class="tok-punctuation">,</span> <span class="tok-typeName">structure</span><span class="tok-punctuation">,</span> <span class="tok-typeName">and</span> <span class="tok-typeName">context</span> <span class="tok-typeName">settings</span><span class="tok-punctuation">;</span> <span class="tok-typeName">clean</span> <span class="tok-typeName">the</span> <span class="tok-typeName">transcript</span> <span class="tok-typeName">to</span> <span class="tok-typeName">match</span> <span class="tok-typeName">those</span></div><div class="cm-line"><span class="tok-typeName">settings</span> <span class="tok-typeName">and</span> <span class="tok-typeName">output</span> <span class="tok-typeName">only</span> <span class="tok-typeName">the</span> <span class="tok-typeName">cleaned</span> <span class="tok-typeName">text</span>.</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then it builds a one-line control header followed by the raw transcript:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-css"><div class="cm-line"><span class="tok-punctuation">[</span><span class="tok-propertyName">Styling</span><span class="tok-punctuation">:</span> <span class="tok-className">semi-formal</span><span class="tok-punctuation">]</span> <span class="tok-punctuation">[</span><span class="tok-propertyName">Structure</span><span class="tok-punctuation">:</span> <span class="tok-className">prose</span><span class="tok-punctuation">]</span> <span class="tok-punctuation">[</span><span class="tok-propertyName">Context</span><span class="tok-punctuation">:</span> <span class="tok-className">general</span><span class="tok-punctuation">]</span></div><div class="cm-line"><span class="tok-typeName">so</span> <span class="tok-typeName">um</span> <span class="tok-typeName">i</span> <span class="tok-typeName">need</span> <span class="tok-typeName">to</span> <span class="tok-typeName">like</span> <span class="tok-typeName">send</span> <span class="tok-typeName">the</span> <span class="tok-typeName">the</span> <span class="tok-typeName">report</span> <span class="tok-typeName">by</span> <span class="tok-typeName">uh</span> <span class="tok-typeName">friday</span> <span class="tok-typeName">no</span> <span class="tok-typeName">wait</span> <span class="tok-typeName">make</span> <span class="tok-typeName">that</span> <span class="tok-typeName">thursday</span></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That control line is what steers the model&#8217;s behavior. The prompt is rendered with the Qwen3 non-thinking chat format, ported verbatim from the model&#8217;s own&nbsp;<code>chat_template.jinja</code>&nbsp;and verified byte-for-byte against the real model. The assistant generation prompt ends with an empty thinking block:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-xml"><div class="cm-line"><span class="tok-punctuation">&lt;</span>|im_start|&gt;assistant</div><div class="cm-line"><span class="tok-punctuation">&lt;</span><span class="tok-typeName">think</span><span class="tok-punctuation">&gt;</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-punctuation">&lt;/</span><span class="tok-typeName">think</span><span class="tok-punctuation">&gt;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That selects Qwen3 non-thinking mode, which is what s1-mini expects.</p>



<p class="wp-block-paragraph">The control values are useful, but they are not all equally literal. Empirical testing showed:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Option</th><th>Value</th><th>Verified behavior</th></tr></thead><tbody><tr><td><code>Styling</code></td><td><code>SemiFormal</code></td><td>Default. Natural cleanup with contractions preserved.</td></tr><tr><td><code>Styling</code></td><td><code>Formal</code></td><td>Distinct. Expands contractions and uses a more formal register.</td></tr><tr><td><code>Styling</code></td><td><code>Casual</code></td><td>Distinct. Keeps more filler/casing/contraction behavior; minimal cleanup.</td></tr><tr><td><code>Structure</code></td><td><code>Prose</code></td><td>Default continuous prose.</td></tr><tr><td><code>Structure</code></td><td><code>Lists</code></td><td>Does not reliably produce Markdown bullets or numbered lists. Do not rely on it for machine-parseable output.</td></tr><tr><td><code>Context</code></td><td><code>General</code></td><td>Default baseline.</td></tr><tr><td><code>Context</code></td><td><code>Email</code></td><td>Distinct. Produces greeting/body/sign-off structure.</td></tr><tr><td><code>Context</code></td><td><code>Message</code></td><td>Accepted, but behaves like&nbsp;<code>General</code>&nbsp;in tested transcripts.</td></tr><tr><td><code>Context</code></td><td><code>Notes</code></td><td>Accepted, but behaves like&nbsp;<code>General</code>&nbsp;in tested transcripts.</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">That table is important because API names can imply more than the model actually guarantees. The docs should reflect measured behavior, not the model card we wish we had.</p>



<p class="wp-block-paragraph">Finally, the upstream quality number should be attributed correctly: Superwhisper reports&nbsp;<strong>94.8% token accuracy on 7,519 held-out English cases</strong>. That is their measurement, not an independent benchmark from this project. This is English only, v1.</p>



<p class="wp-block-paragraph">One further caveat that is easy to get wrong. That figure was measured on Superwhisper&#8217;s official&nbsp;<a href="https://huggingface.co/superwhisper/s1-mini-GGUF">GGUF Q4_K_M build</a>&nbsp;— their model card calls it &#8220;the build the published accuracy was measured on.&#8221; This library runs a different quantization, the INT4 ONNX conversion, and its accuracy has not been separately measured. So 94.8% is a real number about the reference build, not a verified property of the weights this package downloads. Quoting it as the latter would be overclaiming.</p>



<p class="wp-block-paragraph">If you are not on .NET, those official GGUF builds are the better starting point — they work with llama.cpp, Ollama, and LM Studio directly. The ONNX conversion exists specifically because ONNX Runtime GenAI is what .NET can consume.</p>



<p class="wp-block-paragraph">For .NET developers, the practical result is straightforward: if you already have raw ASR text and want local cleanup,&nbsp;<code>ElBruno.S1Mini</code>&nbsp;gives you a small API, a cached local model, and a pipeline that stays on your machine.</p>



<p class="wp-block-paragraph">Links:</p>



<ul class="wp-block-list">
<li>GitHub: <a href="https://github.com/elbruno/ElBruno.S1Mini">https://github.com/elbruno/ElBruno.S1Mini</a></li>



<li>NuGet: <a href="https://www.nuget.org/packages/ElBruno.S1Mini">https://www.nuget.org/packages/ElBruno.S1Mini</a></li>



<li>Upstream model: <a href="https://huggingface.co/superwhisper/s1-mini">https://huggingface.co/superwhisper/s1-mini</a></li>



<li>Official GGUF builds (llama.cpp / Ollama / LM Studio): <a href="https://huggingface.co/superwhisper/s1-mini-GGUF">https://huggingface.co/superwhisper/s1-mini-GGUF</a></li>



<li>ONNX model: <a href="https://huggingface.co/elbruno/s1-mini-onnx">https://huggingface.co/elbruno/s1-mini-onnx</a></li>
</ul>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/08/21/local-transcript-cleanup-in-net-with-s1-mini/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">43714</post-id>
		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.S1Mini/raw/main/docs/blogs/images/before-after.png">
			<media:title type="html">Before and after transcript cleanup examples</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.S1Mini/raw/main/docs/blogs/images/pipeline.png">
			<media:title type="html">Pipeline diagram for microphone to Silero VAD to Whisper to s1-mini</media:title>
		</media:content>
	</item>
		<item>
		<title>Add a Microsoft Foundry model to the GitHub Copilot app</title>
		<link>https://elbruno.com/2026/08/18/add-a-microsoft-foundry-model-to-the-github-copilot-app/</link>
					<comments>https://elbruno.com/2026/08/18/add-a-microsoft-foundry-model-to-the-github-copilot-app/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Tue, 18 Aug 2026 17:32:21 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Azure OpenAI]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[GitHub Copilot]]></category>
		<category><![CDATA[GitHub Copilot App]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[Microsoft Foundry]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=43699</guid>

					<description><![CDATA[### Display name This is the friendly name displayed by the GitHub Copilot app. It does not have to match the Azure model or deployment name: &#8220;`text GPT-5.6-sol on Azure Production reasoning model Foundry GPT-5.6 &#8220;` ### Wire model This is the identifier sent in the `model` property of the request. Azure OpenAI routes requests [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" class="youtube-player" width="640" height="360" src="https://www.youtube.com/embed/cKnNBwpiJdI?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=en&#038;autohide=2&#038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<div class="wp-block-jetpack-markdown"><p>&gt; <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my notes and automate the boring parts, but the geeky fun is 100% mine.</em></p>
<p>Hi!</p>
<p>The GitHub Copilot app can use models from your GitHub Copilot subscription, and it can also connect to models deployed in Microsoft Foundry.</p>
<p>This is a classic <strong>bring your own key</strong>, or BYOK, scenario:</p>
<pre><code class="language-text">GitHub Copilot app
        ↓
Custom Azure OpenAI provider
        ↓
Azure OpenAI v1 Responses API
        ↓
Model deployed in Microsoft Foundry
</code></pre>
<p>In this post, I will configure a <code>gpt-5.6-sol</code> deployment. The same pattern applies to other Azure OpenAI models that support the v1 Responses API.</p>
<p>Let’s do it! <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
</div>



<div class="wp-block-jetpack-markdown"><h2>TL;DR</h2>
<p>This is the provider configuration:</p>
<table>
<thead>
<tr>
<th>GitHub Copilot app setting</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Provider type</td>
<td>Azure OpenAI</td>
</tr>
<tr>
<td>Endpoint</td>
<td><code>https://.services.ai.azure.com/openai/v1/</code></td>
</tr>
<tr>
<td>API version</td>
<td><code>v1</code></td>
</tr>
<tr>
<td>Wire API</td>
<td><code>responses</code></td>
</tr>
<tr>
<td>API key</td>
<td>A key from the Foundry/Azure OpenAI resource</td>
</tr>
<tr>
<td>Custom headers</td>
<td><code>{}</code></td>
</tr>
</tbody>
</table>
<p>This is the model configuration:</p>
<table>
<thead>
<tr>
<th>Model setting</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Display name</td>
<td>Any friendly name, for example <code>gpt-5.6-sol</code></td>
</tr>
<tr>
<td>Wire model</td>
<td>Exact Azure deployment name; optional when it matches the display name</td>
</tr>
<tr>
<td>Reasoning effort</td>
<td>Select the levels supported by the deployed model</td>
</tr>
</tbody>
</table>
<p>The two identifiers that matter are:</p>
<ul>
<li><strong>Endpoint:</strong> identifies the Azure OpenAI resource and API surface.</li>
<li><strong>Wire model:</strong> identifies the deployment inside that resource.</li>
</ul>
<h2>Prerequisites</h2>
<p>Before opening the GitHub Copilot app settings, you need:</p>
<ol>
<li>A Microsoft Foundry or Azure OpenAI resource.</li>
<li>A deployed model that supports the Azure OpenAI v1 API.</li>
<li>The deployment name.</li>
<li>The resource endpoint.</li>
<li>An API key for the resource.</li>
<li>Network access from your computer to the Azure endpoint.</li>
</ol>
<p>For this example:</p>
<pre><code class="language-text">Model:            gpt-5.6-sol
Deployment name:  gpt-5.6-sol
Deployment type:  GlobalStandard
Wire API:         Responses
</code></pre>
<h2>Step 1: open the model deployment in Microsoft Foundry</h2>
<p>Open <a href="https://ai.azure.com/">Microsoft Foundry</a>, select your project or resource, and navigate to:</p>
<pre><code class="language-text">Models + endpoints →  → Details
</code></pre>
<p>The deployment details page contains:</p>
<ul>
<li>the model and deployment name;</li>
<li>the deployment type;</li>
<li>the inference endpoint;</li>
<li>authentication information.</li>
</ul>
</div>



<figure class="wp-block-image size-large"><img loading="lazy" width="963" height="1023" src="https://elbruno.com/wp-content/uploads/2026/08/image.png?w=963" alt="" class="wp-image-43703" srcset="https://elbruno.com/wp-content/uploads/2026/08/image.png?w=963 963w, https://elbruno.com/wp-content/uploads/2026/08/image.png?w=141 141w, https://elbruno.com/wp-content/uploads/2026/08/image.png?w=282 282w, https://elbruno.com/wp-content/uploads/2026/08/image.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/08/image.png 1096w" sizes="auto, (max-width: 963px) 100vw, 963px" /></figure>



<div class="wp-block-jetpack-markdown"><p>The endpoint displayed by Foundry can be the complete operation URL:</p>
<pre><code class="language-text">https://.services.ai.azure.com/openai/v1/responses
</code></pre>
<p>For the GitHub Copilot app provider, use the <strong>base v1 endpoint</strong>:</p>
<pre><code class="language-text">https://.services.ai.azure.com/openai/v1/
</code></pre>
<p>The app adds the Responses operation based on the selected Wire API.</p>
<h3>Resource endpoint versus operation URL</h3>
<p>These values represent different layers:</p>
<pre><code class="language-text">Resource host
https://.services.ai.azure.com

API base path
/openai/v1/

Operation
responses
</code></pre>
<p>Combined, they produce:</p>
<pre><code class="language-text">https://.services.ai.azure.com/openai/v1/responses
</code></pre>
<p>The provider stores the first two parts. The selected Wire API provides the last part.</p>
<h2>Step 2: add an Azure OpenAI provider</h2>
<p>In the GitHub Copilot app, open:</p>
<pre><code class="language-text">Settings → Model providers → Add provider → Azure OpenAI
</code></pre>
<p>Use:</p>
<pre><code class="language-text">Display name:   Azure OpenAI
Endpoint:       https://.services.ai.azure.com/openai/v1/
API version:    v1
Wire API:       responses
API key:        
Custom headers: {}
</code></pre>
</div>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="911" src="https://elbruno.com/wp-content/uploads/2026/08/image-1.png?w=1024" alt="" class="wp-image-43705" srcset="https://elbruno.com/wp-content/uploads/2026/08/image-1.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/08/image-1.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/08/image-1.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/08/image-1.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/08/image-1.png?w=1440 1440w, https://elbruno.com/wp-content/uploads/2026/08/image-1.png 1487w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<div class="wp-block-jetpack-markdown"><h3>Endpoint</h3>
<p>The endpoint must:</p>
<ul>
<li>use the exact hostname assigned to the resource;</li>
<li>include <code>/openai/v1/</code>;</li>
<li>stop at the v1 base path;</li>
<li>not include the deployment name;</li>
<li>not include the API key;</li>
<li>not include <code>/responses</code>.</li>
</ul>
<p>Both Azure OpenAI endpoint formats are supported by the v1 API:</p>
<pre><code class="language-text">https://.openai.azure.com/openai/v1/
</code></pre>
<p>and:</p>
<pre><code class="language-text">https://.services.ai.azure.com/openai/v1/
</code></pre>
<p>Use the hostname shown for your resource in Microsoft Foundry or the Azure portal.</p>
<h3>API version</h3>
<p>Use:</p>
<pre><code class="language-text">v1
</code></pre>
<p>The Azure OpenAI v1 API is a continuously updated API surface. Unlike older Azure OpenAI APIs, it does not require applications to move between monthly dated API versions.</p>
<p>In the GitHub Copilot app provider UI, <code>v1</code> selects the correct API generation for this configuration.</p>
<p>Do not confuse the API version with the model version:</p>
<table>
<thead>
<tr>
<th>Value</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>v1</code></td>
<td>Azure OpenAI API generation</td>
</tr>
<tr>
<td><code>gpt-5.6-sol</code></td>
<td>Model family</td>
</tr>
<tr>
<td><code>2026-07-09</code></td>
<td>Version of the model build</td>
</tr>
<tr>
<td><code>gpt-5.6-sol</code></td>
<td>Deployment name in this example</td>
</tr>
</tbody>
</table>
<h3>Wire API</h3>
<p>Select:</p>
<pre><code class="language-text">responses
</code></pre>
<p>This tells the app to use:</p>
<pre><code class="language-http">POST /openai/v1/responses
</code></pre>
<p>and the Responses API request schema.</p>
<p>Conceptually, the request body is:</p>
<pre><code class="language-json">{
  &quot;model&quot;: &quot;gpt-5.6-sol&quot;,
  &quot;input&quot;: &quot;Hi!&quot;
}
</code></pre>
<p>This is different from the older Chat Completions API, which uses a <code>messages</code> array.</p>
<h3>API key</h3>
<p>Paste one of the keys associated with the same Azure resource as the endpoint.</p>
<p>The important relationship is:</p>
<pre><code class="language-text">API key resource == endpoint resource
</code></pre>
<p>Do not add the key to custom headers or include it in the endpoint.</p>
<h3>Custom headers</h3>
<p>No custom headers are required for standard inference with the v1 Responses API:</p>
<pre><code class="language-json">{}
</code></pre>
<p>Some feature-specific preview APIs use opt-in headers. Basic text generation with a deployed model does not need them.</p>
<h2>Step 3: add the deployed model</h2>
<p>Inside the new Azure OpenAI provider, select <strong>Add model</strong>.</p>
<p>Configure:</p>
<pre><code class="language-text">Display name:      gpt-5.6-sol
Wire model:        
Reasoning effort:  High
</code></pre>
</div>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="852" src="https://elbruno.com/wp-content/uploads/2026/08/image-2.png?w=1024" alt="" class="wp-image-43707" srcset="https://elbruno.com/wp-content/uploads/2026/08/image-2.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/08/image-2.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/08/image-2.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/08/image-2.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/08/image-2.png?w=1440 1440w, https://elbruno.com/wp-content/uploads/2026/08/image-2.png 1473w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><strong>### Display name</strong></p>



<p class="wp-block-paragraph">This is the friendly name displayed by the GitHub Copilot app.</p>



<p class="wp-block-paragraph">It does not have to match the Azure model or deployment name:</p>



<p class="wp-block-paragraph">&#8220;`text</p>



<p class="wp-block-paragraph">GPT-5.6-sol on Azure</p>



<p class="wp-block-paragraph">Production reasoning model</p>



<p class="wp-block-paragraph">Foundry GPT-5.6</p>



<p class="wp-block-paragraph">&#8220;`</p>



<p class="wp-block-paragraph"><strong>### Wire model</strong></p>



<p class="wp-block-paragraph">This is the identifier sent in the `model` property of the request.</p>



<p class="wp-block-paragraph">Azure OpenAI routes requests using the <strong>**deployment name**</strong>, not only the catalog model name.</p>



<p class="wp-block-paragraph">For example, if Foundry contains:</p>



<p class="wp-block-paragraph">&#8220;`text</p>



<p class="wp-block-paragraph">Catalog model: &nbsp; &nbsp;gpt-5.6-sol</p>



<p class="wp-block-paragraph">Deployment name: &nbsp;gpt-5-6-sol-prod</p>



<p class="wp-block-paragraph">&#8220;`</p>



<p class="wp-block-paragraph">configure:</p>



<p class="wp-block-paragraph">&#8220;`text</p>



<p class="wp-block-paragraph">Display name: &nbsp;gpt-5.6-sol</p>



<p class="wp-block-paragraph">Wire model: &nbsp; &nbsp;gpt-5-6-sol-prod</p>



<p class="wp-block-paragraph">&#8220;`</p>



<p class="wp-block-paragraph">The app displays the friendly model name but sends:</p>



<p class="wp-block-paragraph">&#8220;`json</p>



<p class="wp-block-paragraph">{</p>



<p class="wp-block-paragraph">&nbsp; &#8220;model&#8221;: &#8220;gpt-5-6-sol-prod&#8221;</p>



<p class="wp-block-paragraph">}</p>



<p class="wp-block-paragraph">&#8220;`</p>



<p class="wp-block-paragraph">When the display name already matches the deployment name, Wire model can remain empty.</p>



<p class="wp-block-paragraph"><strong>### Token limits</strong></p>



<p class="wp-block-paragraph">The model editor also supports optional prompt and output token limits.</p>



<p class="wp-block-paragraph">Leave these fields empty unless you want the app to apply a limit that is lower than the deployment capability. Azure quotas and model limits continue to apply independently.</p>



<p class="wp-block-paragraph"><strong>### Reasoning effort</strong></p>



<p class="wp-block-paragraph">Select the reasoning levels accepted by the model deployment.</p>



<p class="wp-block-paragraph">For this example, I enabled:</p>



<p class="wp-block-paragraph">&#8220;`text</p>



<p class="wp-block-paragraph">High</p>



<p class="wp-block-paragraph">&#8220;`</p>



<p class="wp-block-paragraph">Reasoning effort affects how much internal reasoning the model can use before returning its answer. Higher effort can improve complex planning and analysis, while increasing latency and token consumption.</p>



<p class="wp-block-paragraph"><strong>## Step 4: save and verify the provider</strong></p>



<p class="wp-block-paragraph">After saving the provider and model, the Model providers page displays the Azure OpenAI provider and its configured model.</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="541" src="https://elbruno.com/wp-content/uploads/2026/08/image-3.png?w=1024" alt="" class="wp-image-43708" srcset="https://elbruno.com/wp-content/uploads/2026/08/image-3.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/08/image-3.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/08/image-3.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/08/image-3.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/08/image-3.png?w=1440 1440w, https://elbruno.com/wp-content/uploads/2026/08/image-3.png 1456w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<div class="wp-block-jetpack-markdown"><p>The provider status indicator should be healthy.</p>
<p>At this point, the app has enough information to construct the complete request:</p>
<pre><code class="language-text">Provider endpoint
https://.services.ai.azure.com/openai/v1/

Wire API
responses

Wire model
gpt-5.6-sol

Authentication
Azure OpenAI resource key
</code></pre>
<h2>Step 5: select the model and send a prompt</h2>
<p>Create a new chat and select the newly configured Azure OpenAI model.</p>
<p>Send a simple prompt:</p>
<pre><code class="language-text">Hi!
</code></pre>
</div>



<figure class="wp-block-image size-large"><img loading="lazy" width="1004" height="477" src="https://elbruno.com/wp-content/uploads/2026/08/image-4.png?w=1004" alt="" class="wp-image-43709" srcset="https://elbruno.com/wp-content/uploads/2026/08/image-4.png 1004w, https://elbruno.com/wp-content/uploads/2026/08/image-4.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/08/image-4.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/08/image-4.png?w=768 768w" sizes="auto, (max-width: 1004px) 100vw, 1004px" /></figure>



<div class="wp-block-jetpack-markdown"><h2>Final configuration</h2>
<p>The final setup is:</p>
<pre><code class="language-text">Microsoft Foundry deployment
        ↓ deployment name

GitHub Copilot app model
        ↓ Wire model

Azure OpenAI provider
        ├── Endpoint: .../openai/v1/
        ├── API version: v1
        ├── Wire API: responses
        └── Authentication: resource API key
</code></pre>
<p>This gives the GitHub Copilot app direct access to your Azure-hosted deployment while keeping model routing, quota, and billing attached to your Microsoft Foundry resource.</p>
<p>Simple, explicit, and ready for more models. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f601.png" alt="😁" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<hr />
<h2>References</h2>
<ul>
<li><a href="https://learn.microsoft.com/azure/foundry/openai/api-version-lifecycle">Azure OpenAI v1 API lifecycle</a></li>
<li><a href="https://learn.microsoft.com/azure/foundry/openai/how-to/responses">Use the Azure OpenAI Responses API</a></li>
<li><a href="https://ai.azure.com/">Microsoft Foundry</a></li>
</ul>
</div>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/08/18/add-a-microsoft-foundry-model-to-the-github-copilot-app/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">43699</post-id>
		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/08/image.png?w=963"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/08/image-1.png?w=1024"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/08/image-2.png?w=1024"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/08/image-3.png?w=1024"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/08/image-4.png?w=1004"/>
	</item>
		<item>
		<title>&#129470; From Model to Agent: let’s build our own Claw, live in C#</title>
		<link>https://elbruno.com/2026/08/10/%f0%9f%a6%be-from-model-to-agent-lets-build-our-own-claw-live-in-c/</link>
					<comments>https://elbruno.com/2026/08/10/%f0%9f%a6%be-from-model-to-agent-lets-build-our-own-claw-live-in-c/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Mon, 10 Aug 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Harness]]></category>
		<category><![CDATA[Microsoft Agent Framework]]></category>
		<category><![CDATA[Microsoft Reactor]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=43692</guid>

					<description/>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="571" src="https://elbruno.com/wp-content/uploads/2026/08/model_2_agent_16-9-v03.png?w=1024" alt="" class="wp-image-43696" srcset="https://elbruno.com/wp-content/uploads/2026/08/model_2_agent_16-9-v03.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/08/model_2_agent_16-9-v03.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/08/model_2_agent_16-9-v03.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/08/model_2_agent_16-9-v03.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/08/model_2_agent_16-9-v03.png 1376w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<div class="wp-block-jetpack-markdown"><p>Hi!</p>
<p>So, you’ve probably used a “claw” already. One of those CLI agents that plans, runs tools, remembers what it did, and more.</p>
<p>Cool. But have you ever stopped to think about what’s actually going on <em>inside</em> one? <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f914.png" alt="🤔" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>Turns out, a claw is really just an <strong>agent harness</strong>: a loop around a model, wired up with tools, planning, memory, approvals and observability. And here’s the fun part: Microsoft Agent Framework now ships that harness for .NET, batteries included.</p>
<p>So instead of 200 slides explaining it… let’s just build one. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f601.png" alt="😁" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>I’m running a new <strong>4-part Microsoft Reactor series for C# and .NET devs</strong>:</p>
<p><strong>From Model to Agent: The Agent Framework Harness, Live in C#</strong></p>
<p><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Register for the full series: <a href="https://aka.ms/mafclaw">aka.ms/mafclaw</a></p>
<p>4 Thursdays, the same agent, one new capability every week. We open the editor, open Copilot, run the thing, break it on purpose (it’s live coding, stuff <em>will</em> break <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" />), fix it, and keep extending it. The running demo is a personal finance CLI assistant, but honestly the patterns work for any agent you want to build.</p>
<p>Here’s the plan <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f447.png" alt="👇" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<h3>1&#x20e3; September 3 · Meet Your Claw: A Harness in Three Lines of C#</h3>
<p>We start with <code>AsHarnessAgent()</code>, add a custom <code>get_stock_price</code> tool, built-in web search, and planning with a todo list. Enough to turn “review my watchlist and suggest something to add” into a real, tracked, step-by-step plan.</p>
<p><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Register: <a href="https://aka.ms/mafclaw/1">aka.ms/mafclaw/1</a></p>
<h3>2&#x20e3; September 10 · Working With Your Data, Safely: Files, Approvals and Memory</h3>
<p>Now it reads a real <code>portfolio.csv</code>, writes reports to disk, puts a <code>place_trade</code> tool behind human approval, and gets memory that actually survives a restart.</p>
<p><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Register: <a href="https://aka.ms/mafclaw/2">aka.ms/mafclaw/2</a></p>
<h3>3&#x20e3; September 17 · Scaling the Claw: Skills, Shell, CodeAct and Background Agents</h3>
<p>This is where it gets spicy. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f60e.png" alt="😎" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Skills, controlled shell access, CodeAct (it writes and runs code instead of guessing at the math), and background agents researching several things at once.</p>
<p><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Register: <a href="https://aka.ms/mafclaw/3">aka.ms/mafclaw/3</a></p>
<h3>4&#x20e3; September 24 · Production Ready: Observability, Governance and Deployment</h3>
<p>The “ok but would you actually ship this?” episode. OpenTelemetry traces, Purview, evaluation, and deploying to Foundry Hosted Agents so it runs as a service instead of a console app.</p>
<p><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Register: <a href="https://aka.ms/mafclaw/4">aka.ms/mafclaw/4</a></p>
<h2><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f468-200d-1f4bb.png" alt="👨‍💻" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Code first, always</h2>
<p>Every session is around 45 to 60 minutes of live coding + Q&amp;A. All C#, all .NET, and you don’t need any previous Agent Framework experience to jump in.</p>
<p><strong>4 Thursdays. 4 live coding sessions. 1 C# claw that keeps growing.</strong></p>
<p>See you there! <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://aka.ms/mafclaw">aka.ms/mafclaw</a></p>
<p><em>(Illustrative sample: the market data is mock, not financial advice.)</em></p>
</div>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/08/10/%f0%9f%a6%be-from-model-to-agent-lets-build-our-own-claw-live-in-c/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">43692</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/08/model_2_agent_16-9-v03.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/08/model_2_agent_16-9-v03.png">
			<media:title type="html">model_2_agent_16-9-v03</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/08/model_2_agent_16-9-v03.png?w=1024"/>
	</item>
		<item>
		<title>&#129504; MagenticBrain Is Now Supported in ElBruno.LocalLLMs</title>
		<link>https://elbruno.com/2026/07/30/%f0%9f%a7%a0-magenticbrain-is-now-supported-in-elbruno-localllms/</link>
					<comments>https://elbruno.com/2026/07/30/%f0%9f%a7%a0-magenticbrain-is-now-supported-in-elbruno-localllms/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Thu, 30 Jul 2026 11:08:07 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Hugging Face]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[local-llm]]></category>
		<category><![CDATA[magentic brain]]></category>
		<category><![CDATA[ONNX]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=42470</guid>

					<description><![CDATA[TL;DR KnownModels.MagenticBrain is now ready for local .NET usage with ElBruno.LocalLLMs. https://huggingface.co/elbruno/MagenticBrain-onnx https://huggingface.co/microsoft/MagenticBrain https://github.com/elbruno/ElBruno.MagenticUI Why this model is important (and what it is designed for) MagenticBrain&#160;is not just another general-purpose chat model. It is designed for&#160;agent orchestration: planning multi-step tasks, selecting tools, chaining tool calls across rounds, and deciding when to terminate with a final answer. That [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/assets/magenticbrain-support/hero-magenticbrain-support.svg" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/magenticbrain-support/hero-magenticbrain-support.svg" alt="MagenticBrain support hero" /></a></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<h2 class="wp-block-heading">TL;DR<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-magenticbrain-support.md#tldr"></a></h2>



<ul class="wp-block-list">
<li><code>KnownModels.MagenticBrain</code> is now ready for local .NET usage with <code>ElBruno.LocalLLMs</code>.</li>



<li>ONNX package is available at:
<ul class="wp-block-list">
<li><a href="https://huggingface.co/elbruno/MagenticBrain-onnx" target="_blank" rel="noopener">https://huggingface.co/elbruno/MagenticBrain-onnx</a></li>
</ul>
</li>



<li>Original model source:
<ul class="wp-block-list">
<li><a href="https://huggingface.co/microsoft/MagenticBrain" target="_blank" rel="noopener">https://huggingface.co/microsoft/MagenticBrain</a></li>
</ul>
</li>



<li>This support is designed to power end-to-end agentic app experiences such as:
<ul class="wp-block-list">
<li><a href="https://github.com/elbruno/ElBruno.MagenticUI" target="_blank" rel="noopener">https://github.com/elbruno/ElBruno.MagenticUI</a></li>
</ul>
</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Why this model is important (and what it is designed for)<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-magenticbrain-support.md#why-this-model-is-important-and-what-it-is-designed-for"></a></h2>



<p class="wp-block-paragraph"><code>MagenticBrain</code>&nbsp;is not just another general-purpose chat model. It is designed for&nbsp;<strong>agent orchestration</strong>: planning multi-step tasks, selecting tools, chaining tool calls across rounds, and deciding when to terminate with a final answer.</p>



<p class="wp-block-paragraph">That design matters because many app scenarios need more than one prompt/one response:</p>



<ul class="wp-block-list">
<li>file + web research workflows</li>



<li>iterative tool usage with state between turns</li>



<li>“do the task, then submit result” orchestration patterns</li>
</ul>



<p class="wp-block-paragraph">In short, MagenticBrain is built for&nbsp;<strong>agentic execution loops</strong>, which is why it is a strong fit for&nbsp;<code>.NET + LocalChatClient + MagenticUI</code>&nbsp;experiences.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<p class="wp-block-paragraph">When Microsoft Research introduced&nbsp;<strong>MagenticLite, MagenticBrain, and Fara1.5</strong>, they framed a practical path for local agentic workflows:</p>



<ul class="wp-block-list">
<li>Official post:
<ul class="wp-block-list">
<li><a href="https://www.microsoft.com/en-us/research/blog/magenticlite-magenticbrain-fara1-5-an-agentic-experience-optimized-for-small-models/" target="_blank" rel="noopener">https://www.microsoft.com/en-us/research/blog/magenticlite-magenticbrain-fara1-5-an-agentic-experience-optimized-for-small-models/</a></li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph">For this repo, this post marks the concrete implementation milestone:&nbsp;<strong>MagenticBrain is now a first-class supported model in&nbsp;<code>ElBruno.LocalLLMs</code></strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Why this matters for MagenticUI scenarios<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-magenticbrain-support.md#why-this-matters-for-magenticui-scenarios"></a></h2>



<p class="wp-block-paragraph">The target is a clean .NET workflow where orchestration and model inference stay in the same stack:</p>



<ol class="wp-block-list">
<li>Use <code>KnownModels.MagenticBrain</code> in <code>LocalChatClient</code>.</li>



<li>Keep tool-calling and multi-round logic in C#.</li>



<li>Reuse the same model path for local multi-agent UX in MagenticUI-style applications.</li>
</ol>



<p class="wp-block-paragraph">This is exactly the scenario behind:</p>



<ul class="wp-block-list">
<li><a href="https://github.com/elbruno/ElBruno.MagenticUI" target="_blank" rel="noopener">https://github.com/elbruno/ElBruno.MagenticUI</a></li>
</ul>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/assets/magenticbrain-support/diagram-magenticbrain-architecture.svg" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/magenticbrain-support/diagram-magenticbrain-architecture.svg" alt="MagenticBrain architecture flow" /></a></figure>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Basic usage: MagenticBrain in C#<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-magenticbrain-support.md#basic-usage-magenticbrain-in-c"></a></h2>



<h3 class="wp-block-heading">1. Install<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-magenticbrain-support.md#1-install"></a></h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet add package ElBruno.LocalLLMs --version 0.20.4</div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">2. Create a local MagenticBrain client<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-magenticbrain-support.md#2-create-a-local-magenticbrain-client"></a></h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">LocalLLMs</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">Microsoft</span>.<span class="tok-variableName">Extensions</span>.<span class="tok-variableName">AI</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">options</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">LocalLLMsOptions</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">Model</span> <span class="tok-operator">=</span> <span class="tok-variableName">KnownModels</span>.<span class="tok-variableName">MagenticBrain</span>,</div><div class="cm-line">    <span class="tok-variableName">EnsureModelDownloaded</span> <span class="tok-operator">=</span> <span class="tok-atom">true</span>,</div><div class="cm-line">    <span class="tok-variableName">Temperature</span> <span class="tok-operator">=</span> <span class="tok-number">0.7f</span>,</div><div class="cm-line">    <span class="tok-variableName">MaxSequenceLength</span> <span class="tok-operator">=</span> <span class="tok-number">32768</span></div><div class="cm-line">};</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">client</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">LocalChatClient</span>.<span class="tok-variableName">CreateAsync</span>(<span class="tok-variableName">options</span>);</div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">3. Use it in an agentic loop with tools<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-magenticbrain-support.md#3-use-it-in-an-agentic-loop-with-tools"></a></h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">response</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">client</span>.<span class="tok-variableName">GetResponseAsync</span>(</div><div class="cm-line">[</div><div class="cm-line">    <span class="tok-keyword">new</span> <span class="tok-variableName">ChatMessage</span>(<span class="tok-variableName">ChatRole</span>.<span class="tok-variableName">System</span>, <span class="tok-string">&quot;You are an agentic assistant. Use tools and call submit when done.&quot;</span>),</div><div class="cm-line">    <span class="tok-keyword">new</span> <span class="tok-variableName">ChatMessage</span>(<span class="tok-variableName">ChatRole</span>.<span class="tok-variableName">User</span>, <span class="tok-string">&quot;List project files and summarize README.&quot;</span>)</div><div class="cm-line">],</div><div class="cm-line"><span class="tok-keyword">new</span> <span class="tok-variableName">ChatOptions</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">Tools</span> <span class="tok-operator">=</span> <span class="tok-variableName">tools</span></div><div class="cm-line">});</div></code></pre>
		</div>
	</div>
</div>


<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/assets/magenticbrain-support/diagram-magenticbrain-rounds.svg" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/magenticbrain-support/diagram-magenticbrain-rounds.svg" alt="MagenticBrain agent round lifecycle" /></a></figure>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Repo samples you can run now<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-magenticbrain-support.md#repo-samples-you-can-run-now"></a></h2>



<ul class="wp-block-list">
<li><code><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/src/samples/MagenticBrainAgent" target="_blank" rel="noopener">src/samples/MagenticBrainAgent</a></code>
<ul class="wp-block-list">
<li>OmniAgent-style round loop, tool calls, and <code>submit</code> stop-signal.</li>
</ul>
</li>



<li><code><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/src/samples/MagenticUIServer" target="_blank" rel="noopener">src/samples/MagenticUIServer</a></code>
<ul class="wp-block-list">
<li>Local multi-agent web sample with SignalR + React UI.</li>
</ul>
</li>



<li><code><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/magentic-ui-dotnet.md" target="_blank" rel="noopener">docs/magentic-ui-dotnet.md</a></code>
<ul class="wp-block-list">
<li>Architecture and setup details for MagenticUI .NET flow.</li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph">No extra sample project is required for this post: the existing MagenticBrain and MagenticUI samples already cover the runnable story.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Sample app screenshots (MagenticUIServer)<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-magenticbrain-support.md#sample-app-screenshots-magenticuiserver"></a></h2>



<p class="wp-block-paragraph">The following screenshots illustrate the sample app flow using the Magentic UI client and agent stream model:</p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/assets/magenticbrain-support/screenshot-magenticui-connected.svg" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/magenticbrain-support/screenshot-magenticui-connected.svg" alt="MagenticUI sample connection and task submission" /></a></figure>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/assets/magenticbrain-support/screenshot-magenticui-rounds.svg" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/magenticbrain-support/screenshot-magenticui-rounds.svg" alt="MagenticUI sample multi-round agent progress" /></a></figure>



<p class="wp-block-paragraph">You can run the sample from:</p>



<ul class="wp-block-list">
<li><code><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/src/samples/MagenticUIServer/MagenticUIServer" target="_blank" rel="noopener">src/samples/MagenticUIServer/MagenticUIServer</a></code></li>



<li><code><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/src/samples/MagenticUIServer/MagenticUIServer/ClientApp" target="_blank" rel="noopener">src/samples/MagenticUIServer/MagenticUIServer/ClientApp</a></code></li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Relevant links<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-magenticbrain-support.md#relevant-links"></a></h2>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.MagenticUI" target="_blank" rel="noopener">https://github.com/elbruno/ElBruno.MagenticUI</a><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-magenticbrain-support.md#-magenticbrain-is-now-supported-in-elbrunolocalllms"></a></p>



<p class="wp-block-paragraph">NuGet: <a href="https://www.nuget.org/packages/ElBruno.LocalLLMs">https://www.nuget.org/packages/ElBruno.LocalLLMs</a></p>



<p class="wp-block-paragraph">Repository: <a href="https://github.com/elbruno/ElBruno.LocalLLMs">https://github.com/elbruno/ElBruno.LocalLLMs</a></p>



<p class="wp-block-paragraph">Supported models reference:</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/supported-models.md">https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/supported-models.md</a></p>



<p class="wp-block-paragraph">Auto-download guide:</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/auto-download.md">https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/auto-download.md</a></p>



<p class="wp-block-paragraph">Official Microsoft announcement:</p>



<p class="wp-block-paragraph"><a href="https://www.microsoft.com/en-us/research/blog/magenticlite-magenticbrain-fara1-5-an-agentic-experience-optimized-for-small-models/">https://www.microsoft.com/en-us/research/blog/magenticlite-magenticbrain-fara1-5-an-agentic-experience-optimized-for-small-models/</a></p>



<p class="wp-block-paragraph">Original MagenticBrain model:</p>



<p class="wp-block-paragraph"><a href="https://huggingface.co/microsoft/MagenticBrain">https://huggingface.co/microsoft/MagenticBrain</a></p>



<p class="wp-block-paragraph">Published ONNX package used by this repo:</p>



<p class="wp-block-paragraph"><a href="https://huggingface.co/elbruno/MagenticBrain-onnx">https://huggingface.co/elbruno/MagenticBrain-onnx</a></p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/07/30/%f0%9f%a7%a0-magenticbrain-is-now-supported-in-elbruno-localllms/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">42470</post-id>
		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/magenticbrain-support/hero-magenticbrain-support.svg">
			<media:title type="html">MagenticBrain support hero</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/magenticbrain-support/diagram-magenticbrain-architecture.svg">
			<media:title type="html">MagenticBrain architecture flow</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/magenticbrain-support/diagram-magenticbrain-rounds.svg">
			<media:title type="html">MagenticBrain agent round lifecycle</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/magenticbrain-support/screenshot-magenticui-connected.svg">
			<media:title type="html">MagenticUI sample connection and task submission</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/magenticbrain-support/screenshot-magenticui-rounds.svg">
			<media:title type="html">MagenticUI sample multi-round agent progress</media:title>
		</media:content>
	</item>
		<item>
		<title>&#128640; Fara 1.5-9B Is Now Fully Supported in ElBruno.LocalLLMs</title>
		<link>https://elbruno.com/2026/07/28/%f0%9f%9a%80-fara-1-5-9b-is-now-fully-supported-in-elbruno-localllms/</link>
					<comments>https://elbruno.com/2026/07/28/%f0%9f%9a%80-fara-1-5-9b-is-now-fully-supported-in-elbruno-localllms/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Tue, 28 Jul 2026 18:37:20 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Fara]]></category>
		<category><![CDATA[Hugging Face]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[local-llm]]></category>
		<category><![CDATA[magenticui]]></category>
		<category><![CDATA[ONNX]]></category>
		<category><![CDATA[ONNXRuntime]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=42465</guid>

					<description><![CDATA[TL;DR Fara1.5-9B now works end-to-end in C# with ElBruno.LocalLLMs. https://huggingface.co/elbruno/Fara1.5-9B-onnx You can run image+text inference locally with LocalVisionChatClient and KnownModels.Fara15_9B. https://github.com/elbruno/ElBruno.MagenticUI When Microsoft Research introduced&#160;MagenticLite, MagenticBrain, and Fara1.5, it made a strong case for practical local agentic experiences: Original announcement: https://www.microsoft.com/en-us/research/blog/magenticlite-magenticbrain-fara1-5-an-agentic-experience-optimized-for-small-models/ In plain words: smaller, specialized models can deliver real agentic behavior when the tooling is right. For this repo, that “tooling [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-fara-support.md#-fara-15-9b-is-now-fully-supported-in-elbrunolocalllms"></a></p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/assets/fara-support/hero-fara-support.svg" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/fara-support/hero-fara-support.svg" alt="Fara support hero" /></a></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<h2 class="wp-block-heading">TL;DR<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-fara-support.md#tldr"></a></h2>



<ul class="wp-block-list">
<li><code>Fara1.5-9B</code> now works end-to-end in <strong>C#</strong> with <code>ElBruno.LocalLLMs</code>.</li>



<li>The validated multimodal ONNX package is published at:
<ul class="wp-block-list">
<li><a href="https://huggingface.co/elbruno/Fara1.5-9B-onnx" target="_blank" rel="noopener">https://huggingface.co/elbruno/Fara1.5-9B-onnx</a></li>
</ul>
</li>



<li>You can run image+text inference locally with <code>LocalVisionChatClient</code> and <code>KnownModels.Fara15_9B</code>.</li>



<li>This closes the loop for end-to-end local agent experiences in:
<ul class="wp-block-list">
<li><a href="https://github.com/elbruno/ElBruno.MagenticUI" target="_blank" rel="noopener">https://github.com/elbruno/ElBruno.MagenticUI</a></li>
</ul>
</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<p class="wp-block-paragraph">When Microsoft Research introduced&nbsp;<strong>MagenticLite, MagenticBrain, and Fara1.5</strong>, it made a strong case for practical local agentic experiences:</p>



<ul class="wp-block-list">
<li>Original announcement: <a href="https://www.microsoft.com/en-us/research/blog/magenticlite-magenticbrain-fara1-5-an-agentic-experience-optimized-for-small-models/" target="_blank" rel="noopener">https://www.microsoft.com/en-us/research/blog/magenticlite-magenticbrain-fara1-5-an-agentic-experience-optimized-for-small-models/</a></li>
</ul>



<p class="wp-block-paragraph">In plain words: smaller, specialized models can deliver real agentic behavior when the tooling is right.</p>



<p class="wp-block-paragraph">For this repo, that “tooling is right” moment means:&nbsp;<strong>Fara is now actually runnable end-to-end in .NET</strong>, not just listed as an idea.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Why this matters for C# end-to-end<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-fara-support.md#why-this-matters-for-c-end-to-end"></a></h2>



<p class="wp-block-paragraph">The goal of&nbsp;<code>ElBruno.LocalLLMs</code>&nbsp;is to make local model usage feel native in .NET. With Fara support in place:</p>



<ol class="wp-block-list">
<li>You can auto-download the model from Hugging Face.</li>



<li>Run vision prompts with <code>LocalVisionChatClient</code>.</li>



<li>Keep everything in a C# workflow without external service dependencies.</li>
</ol>



<p class="wp-block-paragraph">That is especially important for app-level experiences like:</p>



<ul class="wp-block-list">
<li><a href="https://github.com/elbruno/ElBruno.MagenticUI" target="_blank" rel="noopener">https://github.com/elbruno/ElBruno.MagenticUI</a></li>
</ul>



<p class="wp-block-paragraph">where “agent loop + UI + model inference” should stay coherent in one stack.</p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/assets/fara-support/diagram-fara-architecture.svg" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/fara-support/diagram-fara-architecture.svg" alt="Fara architecture flow" /></a></figure>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Basic usage: Fara in C#<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-fara-support.md#basic-usage-fara-in-c"></a></h2>



<h3 class="wp-block-heading">1. Install<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-fara-support.md#1-install"></a></h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet add package ElBruno.LocalLLMs --version 0.20.3</div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">2. Minimal Fara vision client<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-fara-support.md#2-minimal-fara-vision-client"></a></h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">LocalLLMs</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">Microsoft</span>.<span class="tok-variableName">Extensions</span>.<span class="tok-variableName">AI</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">options</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">LocalLLMsOptions</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">Model</span> <span class="tok-operator">=</span> <span class="tok-variableName">KnownModels</span>.<span class="tok-variableName">Fara15_9B</span>,</div><div class="cm-line">    <span class="tok-variableName">EnsureModelDownloaded</span> <span class="tok-operator">=</span> <span class="tok-atom">true</span>,</div><div class="cm-line">    <span class="tok-variableName">ExecutionProvider</span> <span class="tok-operator">=</span> <span class="tok-variableName">ExecutionProvider</span>.<span class="tok-variableName">Cpu</span></div><div class="cm-line">};</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">await</span> <span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">client</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">LocalVisionChatClient</span>(<span class="tok-variableName">options</span>);</div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">3. Ask Fara about an image<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-fara-support.md#3-ask-fara-about-an-image"></a></h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">response</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">client</span>.<span class="tok-variableName">GetResponseAsync</span>(</div><div class="cm-line">[</div><div class="cm-line">    <span class="tok-keyword">new</span> <span class="tok-variableName">ChatMessage</span>(<span class="tok-variableName">ChatRole</span>.<span class="tok-variableName">User</span>, <span class="tok-string">&quot;Describe the image in one sentence.&quot;</span>)</div><div class="cm-line">],</div><div class="cm-line"><span class="tok-keyword">new</span> <span class="tok-variableName">VisionChatOptions</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">ImagePaths</span> <span class="tok-operator">=</span> [<span class="tok-string">@&quot;C:\images\screen.png&quot;</span>],</div><div class="cm-line">    <span class="tok-variableName">MaxOutputTokens</span> <span class="tok-operator">=</span> <span class="tok-number">64</span></div><div class="cm-line">});</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">response</span>.<span class="tok-variableName">Text</span>);</div></code></pre>
		</div>
	</div>
</div>


<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Reusing existing Fara sample code</h2>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-fara-support.md#reusing-existing-fara-sample-code"></a></p>



<p class="wp-block-paragraph">Fara-specific sample code is already in the repo:</p>



<ul class="wp-block-list">
<li><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/src/samples/FaraVisionAgent"><code>src/samples/FaraVisionAgent</code></a></li>
</ul>



<p class="wp-block-paragraph">This sample already covers:</p>



<ul class="wp-block-list">
<li>auto-download from <code>elbruno/Fara1.5-9B-onnx</code></li>



<li>local <code>--model-path</code> override</li>



<li><code>--image-path</code> flow with <code>VisionChatOptions</code></li>
</ul>



<p class="wp-block-paragraph">If you want to start from runnable code instead of snippets, use that sample first.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Validation snapshot<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-fara-support.md#validation-snapshot"></a></h2>



<p class="wp-block-paragraph">Focused validation for Fara support passed:</p>



<ul class="wp-block-list">
<li>Fara unit tests: <strong>39/39</strong></li>



<li>Fara Hugging Face checks: <strong>3/3</strong></li>



<li>Fara vision lifecycle E2E: <strong>pass</strong>
<ul class="wp-block-list">
<li>download -&gt; inference -&gt; cache hit -&gt; delete</li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph">Detailed report:</p>



<ul class="wp-block-list">
<li><code><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/tests/2026-07-28-15-run-results.md" target="_blank" rel="noopener">docs/tests/2026-07-28-15-run-results.md</a></code></li>



<li>Issue tracking (closed): <a href="https://github.com/elbruno/ElBruno.LocalLLMs/issues/35">#35</a></li>
</ul>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/assets/fara-support/diagram-fara-lifecycle.svg" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/fara-support/diagram-fara-lifecycle.svg" alt="Lifecycle coverage" /></a></figure>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Relevant links<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog/blog-fara-support.md#relevant-links"></a></h2>



<ul class="wp-block-list">
<li>NuGet: <a href="https://www.nuget.org/packages/ElBruno.LocalLLMs" target="_blank" rel="noopener">https://www.nuget.org/packages/ElBruno.LocalLLMs</a></li>



<li>Repo: <a href="https://github.com/elbruno/ElBruno.LocalLLMs" target="_blank" rel="noopener">https://github.com/elbruno/ElBruno.LocalLLMs</a></li>



<li>Fara ONNX package: <a href="https://huggingface.co/elbruno/Fara1.5-9B-onnx" target="_blank" rel="noopener">https://huggingface.co/elbruno/Fara1.5-9B-onnx</a></li>



<li>Microsoft announcement:
<ul class="wp-block-list">
<li><a href="https://www.microsoft.com/en-us/research/blog/magenticlite-magenticbrain-fara1-5-an-agentic-experience-optimized-for-small-models/" target="_blank" rel="noopener">https://www.microsoft.com/en-us/research/blog/magenticlite-magenticbrain-fara1-5-an-agentic-experience-optimized-for-small-models/</a></li>
</ul>
</li>



<li>MagenticUI .NET app:
<ul class="wp-block-list">
<li><a href="https://github.com/elbruno/ElBruno.MagenticUI" target="_blank" rel="noopener">https://github.com/elbruno/ElBruno.MagenticUI</a></li>
</ul>
</li>
</ul>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/07/28/%f0%9f%9a%80-fara-1-5-9b-is-now-fully-supported-in-elbruno-localllms/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">42465</post-id>
		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/fara-support/hero-fara-support.svg">
			<media:title type="html">Fara support hero</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/fara-support/diagram-fara-architecture.svg">
			<media:title type="html">Fara architecture flow</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.LocalLLMs/raw/main/docs/blog/assets/fara-support/diagram-fara-lifecycle.svg">
			<media:title type="html">Lifecycle coverage</media:title>
		</media:content>
	</item>
		<item>
		<title>&#129504; Someone Asked Me to Add a 975B Model to a LocaLLMs, here’s Why we Can’t (Yet)</title>
		<link>https://elbruno.com/2026/07/16/%f0%9f%a7%a0-someone-asked-me-to-add-a-975b-model-to-a-locallms-heres-why-we-cant-yet/</link>
					<comments>https://elbruno.com/2026/07/16/%f0%9f%a7%a0-someone-asked-me-to-add-a-975b-model-to-a-locallms-heres-why-we-cant-yet/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Thu, 16 Jul 2026 15:24:13 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Inkling]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[LocalLLMs]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=42461</guid>

					<description><![CDATA[⚠️&#160;This blog post was created with the help of AI tools. The geeky fun and the 🤖 in C# are 100% mine. TL;DR Someone asked me to add&#160;Inkling&#160;— Thinking Machines&#8217; shiny new open-weights model — to&#160;ElBruno.LocalLLMs. I did the evaluation, and the answer is:&#160;not for now.&#160;🔴 Inkling is a&#160;975B-parameter, Mixture-of-Experts, multimodal&#160;(text + image + audio) [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" width="512" height="512" src="https://elbruno.com/wp-content/uploads/2026/07/blog-inkling-support-small.png?w=512" alt="" class="wp-image-42463" srcset="https://elbruno.com/wp-content/uploads/2026/07/blog-inkling-support-small.png 512w, https://elbruno.com/wp-content/uploads/2026/07/blog-inkling-support-small.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/07/blog-inkling-support-small.png?w=300 300w" sizes="auto, (max-width: 512px) 100vw, 512px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" />&nbsp;<em>This blog post was created with the help of AI tools. The geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">TL;DR<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-inkling-support.md#tldr"></a></h2>



<p class="wp-block-paragraph">Someone asked me to add&nbsp;<strong><a href="https://huggingface.co/thinkingmachines/Inkling">Inkling</a></strong>&nbsp;— Thinking Machines&#8217; shiny new open-weights model — to&nbsp;<strong>ElBruno.LocalLLMs</strong>. I did the evaluation, and the answer is:&nbsp;<strong>not for now.</strong>&nbsp;<img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f534.png" alt="🔴" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">Inkling is a&nbsp;<strong>975B-parameter, Mixture-of-Experts, multimodal</strong>&nbsp;(text + image + audio) model. My library runs&nbsp;<em>local</em>&nbsp;LLMs in .NET through ONNX Runtime GenAI, and Inkling hits&nbsp;<strong>every</strong>&nbsp;wall at once:</p>



<ul class="wp-block-list">
<li><strong>MoE routing</strong> isn&#8217;t supported by the ONNX GenAI builder.</li>



<li><strong>Multimodal in/out</strong> has no path through the text-only conversion pipeline.</li>



<li><strong>Size:</strong> ~490 GB+ of weights <em>even at INT4</em> — this is a data-center model, not a laptop model.</li>



<li><strong>NVFP4 numerics</strong> + custom architecture = no conversion path exists.</li>
</ul>



<p class="wp-block-paragraph">So I documented it honestly as&nbsp;<strong>Not Viable</strong>&nbsp;in&nbsp;<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blocked-models.md"><code>blocked-models.md</code></a>, pointed folks at hosted APIs for Inkling itself, and recommended small local alternatives (<strong>Phi-4</strong>,&nbsp;<strong>Qwen2.5-32B</strong>,&nbsp;<strong>DeepSeek-R1-Distill-Qwen-14B</strong>). If you want it on your own hardware today — you&#8217;d need a rack, not a ThinkPad. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The Longer Story (thanks Copilot!)<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-inkling-support.md#the-longer-story"></a></h2>



<p class="wp-block-paragraph">Hi! <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f44b.png" alt="👋" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">Every now and then someone opens an issue or drops me a message:&nbsp;<em>&#8220;Hey Bruno, can you add&nbsp;<strong>&lt;shiny new model&gt;</strong>&nbsp;to your library?&#8221;</em>&nbsp;I love those messages. It means people are actually using&nbsp;<strong>ElBruno.LocalLLMs</strong>&nbsp;to run models locally in .NET, and they want the latest and greatest.</p>



<p class="wp-block-paragraph">This time the request was&nbsp;<strong>Inkling</strong>, from Thinking Machines. And it&nbsp;<em>is</em>&nbsp;a fascinating model. But after digging in, I had to give the answer nobody likes to hear:&nbsp;<strong>not right now.</strong>&nbsp;Let me walk you through&nbsp;<em>why</em>&nbsp;— because the &#8220;why&#8221; is genuinely interesting, and it says a lot about the difference between a&nbsp;<strong>frontier cloud model</strong>&nbsp;and a&nbsp;<strong>local model</strong>.</p>



<h3 class="wp-block-heading">First, what is Inkling?<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-inkling-support.md#first-what-is-inkling"></a></h3>



<p class="wp-block-paragraph">Inkling is a general-purpose,&nbsp;<strong>natively multimodal</strong>&nbsp;model. It takes&nbsp;<strong>text, images, and audio</strong>&nbsp;as input and produces text output. Under the hood:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Property</th><th>Value</th></tr></thead><tbody><tr><td><strong>Parameters</strong></td><td>975B total, 41B active</td></tr><tr><td><strong>Architecture</strong></td><td>66-layer decoder-only transformer</td></tr><tr><td><strong>Feed-forward</strong></td><td>Sparse&nbsp;<strong>Mixture-of-Experts</strong>&nbsp;— 6 of 256 experts routed + 2 shared per token</td></tr><tr><td><strong>Attention</strong></td><td>Hybrid local + global layers</td></tr><tr><td><strong>Vision</strong></td><td>Hierarchical patch encoder</td></tr><tr><td><strong>Audio</strong></td><td>Discrete token encoding</td></tr><tr><td><strong>Numerics</strong></td><td>BF16 and NVFP4</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">On paper? Gorgeous. It&#8217;s an impressive piece of engineering, and it benchmarks right alongside the big closed models. But almost every one of those beautiful properties is a&nbsp;<strong>brick wall</strong>&nbsp;for local inference.</p>



<h3 class="wp-block-heading">What my library actually does<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-inkling-support.md#what-my-library-actually-does"></a></h3>



<p class="wp-block-paragraph">Quick reminder on how&nbsp;<strong>ElBruno.LocalLLMs</strong>&nbsp;works: it lets you run local LLMs in .NET through the standard&nbsp;<code>IChatClient</code>&nbsp;interface, powered by&nbsp;<strong>ONNX Runtime GenAI</strong>. To add a model, I:</p>



<ol class="wp-block-list">
<li>Convert its weights to <strong>ONNX GenAI format</strong> (usually via <code>optimum</code> / the <code>onnxruntime-genai</code> builder).</li>



<li>Register a <code>ModelDefinition</code> in <code>KnownModels.cs</code>.</li>



<li>Ship it so you can <code>dotnet add package</code> and go.</li>
</ol>



<p class="wp-block-paragraph">The whole design assumes a model you can realistically&nbsp;<strong>download and run on your own machine</strong>&nbsp;— a laptop, a workstation, maybe a single GPU. That assumption is the crux of the problem.</p>



<h3 class="wp-block-heading">The four walls <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f9f1.png" alt="🧱" class="wp-smiley" style="height: 1em; max-height: 1em;" /><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-inkling-support.md#the-four-walls-"></a></h3>



<p class="wp-block-paragraph"><strong>Wall #1 — Mixture of Experts.</strong> ONNX Runtime GenAI&#8217;s model builder doesn&#8217;t support MoE routing (or maybe I am missing something). It can&#8217;t express &#8220;pick 6 of 256 experts per token.&#8221; This is the <em>same</em> blocker that keeps Mixtral and DeepSeek-V3 out of the library. Inkling just has a <em>lot</em> more experts.</p>



<p class="wp-block-paragraph"><strong>Wall #2 — Multimodal in/out.</strong>&nbsp;My conversion path is&nbsp;<strong>text-generation only</strong>&nbsp;(<code>text-generation-with-past</code>). Inkling&#8217;s vision patch encoder and discrete audio tokenizer have nowhere to go in that pipeline. Even if the text backbone converted cleanly, the image and audio front-ends wouldn&#8217;t.</p>



<p class="wp-block-paragraph"><strong>Wall #3 — Size.</strong>&nbsp;This is the big one. 975B parameters is roughly&nbsp;<strong>~2 TB in BF16</strong>. Quantize it all the way down to INT4 and you&#8217;re&nbsp;<em>still</em>&nbsp;looking at&nbsp;<strong>~490 GB+ of weights</strong>&nbsp;— plus multiple terabytes of RAM just to&nbsp;<em>run the conversion</em>. For inference you&#8217;d need hundreds of GB of VRAM across multiple data-center GPUs. That&#8217;s not &#8220;local&#8221; by any definition I can ship to you.</p>



<p class="wp-block-paragraph"><strong>Wall #4 — Numerics + custom architecture.</strong>&nbsp;Inkling ships in BF16 and&nbsp;<strong>NVFP4</strong>&nbsp;(an NVIDIA 4-bit format, not ONNX). Combined with its custom multimodal MoE design, there&#8217;s simply&nbsp;<strong>no export path</strong>&nbsp;today.</p>



<p class="wp-block-paragraph">Any&nbsp;<em>one</em>&nbsp;of these would be enough to block it. Inkling has all four. It&#8217;s essentially a bigger, multimodal sibling of DeepSeek-V3 (671B MoE), which I&#8217;d already marked as&nbsp;<strong>Not Viable</strong>&nbsp;for the same reasons.</p>



<h3 class="wp-block-heading">So what did I actually do?<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-inkling-support.md#so-what-did-i-actually-do"></a></h3>



<p class="wp-block-paragraph">I didn&#8217;t want this to be a silent &#8220;no.&#8221; So I:</p>



<ul class="wp-block-list">
<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Ran the full evaluation and wrote it up in <strong><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blocked-models.md"><code>docs/blocked-models.md</code></a></strong> with a proper blocker table.</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Added a heads-up (with a link) in <strong><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/supported-models.md"><code>docs/supported-models.md</code></a></strong> so nobody wastes an afternoon trying to convert it.</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Logged it in the changelog.</li>
</ul>



<p class="wp-block-paragraph">And most importantly —&nbsp;<strong>alternatives</strong>:</p>



<ul class="wp-block-list">
<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Want Inkling specifically?</strong> Use it via a <strong>hosted API</strong> (the Tinker cookbook or a third-party inference provider). That&#8217;s what it&#8217;s built for.</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Want something great that runs locally in .NET today?</strong> Try <strong>Phi-4</strong> (14B), <strong>Qwen2.5-32B</strong>, or <strong>DeepSeek-R1-Distill-Qwen-14B</strong>. All native ONNX, all ready to go with <code>KnownModels</code>.</li>
</ul>



<h3 class="wp-block-heading">The takeaway<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-inkling-support.md#the-takeaway"></a></h3>



<p class="wp-block-paragraph">Not every frontier model is meant to run on your machine — and that&#8217;s okay. Part of maintaining a&nbsp;<em>local</em>&nbsp;LLM library is being honest about where &#8220;local&#8221; ends and &#8220;data center&#8221; begins. Inkling is a spectacular model. It&#8217;s just not a&nbsp;<strong>local</strong>&nbsp;one.</p>



<p class="wp-block-paragraph">If the ONNX Runtime GenAI builder adds MoE + multimodal support down the line, I&#8217;ll revisit. Until then, Inkling stays on the &#8220;not viable&#8221; list — documented, explained, and with good alternatives right next to it.</p>



<p class="wp-block-paragraph">Thanks for the request, whoever you are. Keep &#8217;em coming. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/07/16/%f0%9f%a7%a0-someone-asked-me-to-add-a-975b-model-to-a-locallms-heres-why-we-cant-yet/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">42461</post-id>
		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/07/blog-inkling-support-small.png?w=512"/>
	</item>
		<item>
		<title>I built a weekly AI news digest with GitHub Agentic Workflows. Here is how it works.</title>
		<link>https://elbruno.com/2026/07/10/i-built-a-weekly-ai-news-digest-with-github-agentic-workflows-here-is-how-it-works/</link>
					<comments>https://elbruno.com/2026/07/10/i-built-a-weekly-ai-news-digest-with-github-agentic-workflows-here-is-how-it-works/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Fri, 10 Jul 2026 22:46:16 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[GitHub]]></category>
		<category><![CDATA[GitHub Actions]]></category>
		<category><![CDATA[GitHub Agentic Workflows]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40769</guid>

					<description><![CDATA[This blog post was created with the help of AI tools. I used them to organize ideas and write a first draft, but the workflow, the code, the testing, and the 🤖 enthusiasm are mine. Hi! I like reading AI news. I do not like opening seven tabs, comparing five versions of the same announcement, [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/2026-07-10-how-agentic-workflow-works.md#i-built-a-weekly-ai-news-digest-with-github-agentic-workflows-here-is-how-it-works"></a></p>



<p class="wp-block-paragraph"><em>This blog post was created with the help of AI tools. I used them to organize ideas and write a first draft, but the workflow, the code, the testing, and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> enthusiasm are mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">I like reading AI news. I do not like opening seven tabs, comparing five versions of the same announcement, and wondering whether the most important developer update was hidden under an article about a robot making coffee.</p>



<p class="wp-block-paragraph">So I built a small project: <a href="https://github.com/elbruno/weekly-ai-news-digest" target="_blank" rel="noopener">Weekly AI News Digest</a>.</p>



<p class="wp-block-paragraph">It is a static site that collects relevant AI and technology stories, turns them into a readable digest, and publishes the result with GitHub Pages. The interesting part is not the HTML page. The interesting part is that the workflow is driven by <a href="https://github.github.com/gh-aw/" target="_blank" rel="noopener">GitHub Agentic Workflows</a>.</p>



<p class="wp-block-paragraph">In other words: an agent researches, curates, writes the page, and proposes the change. GitHub still keeps the dangerous part of the process behind a controlled pull request.</p>



<p class="wp-block-paragraph">That balance is the whole point.</p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/assets/agentic-workflow-hero.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/weekly-ai-news-digest/raw/main/docs/blog/assets/agentic-workflow-hero.png" alt="Generated illustration of an AI-news workflow moving through a review checkpoint to a web digest." /></a></figure>



<p class="wp-block-paragraph"><em>Generated illustration. It represents the workflow concept and is not a screenshot of the product.</em></p>



<h2 class="wp-block-heading">I did not want another news scraper<a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/2026-07-10-how-agentic-workflow-works.md#i-did-not-want-another-news-scraper"></a></h2>



<p class="wp-block-paragraph">A normal news scraper is easy to imagine:</p>



<ol class="wp-block-list">
<li>Fetch RSS feeds.</li>



<li>Print links.</li>



<li>Add a sad <code>README.md</code>.</li>



<li>Promise to improve it later.</li>
</ol>



<p class="wp-block-paragraph">The problem is that a list of links is not a digest.</p>



<p class="wp-block-paragraph">For a weekly briefing, I want context:</p>



<ul class="wp-block-list">
<li>What happened?</li>



<li>Why should a developer care?</li>



<li>Which stories are actually important?</li>



<li>Which ones are related to GitHub, Copilot, developer tools, security, or AI platforms?</li>
</ul>



<p class="wp-block-paragraph">That last question matters to me. I wanted a GitHub-first lens. Not because GitHub is the only place where AI happens, but because it is where a lot of us build, review, ship, automate, and occasionally ask a coding agent to refactor half the repository before coffee.</p>



<p class="wp-block-paragraph">The result is a small, focused site instead of an infinite stream.</p>



<h2 class="wp-block-heading">The workflow is a Markdown file<a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/2026-07-10-how-agentic-workflow-works.md#the-workflow-is-a-markdown-file"></a></h2>



<p class="wp-block-paragraph">The core of the project lives in <code><a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/.github/workflows/weekly-news-digest.md" target="_blank" rel="noopener">.github/workflows/weekly-news-digest.md</a></code>.</p>



<p class="wp-block-paragraph">Yes, a Markdown file.</p>



<p class="wp-block-paragraph">GitHub Agentic Workflows use a Markdown-based definition: frontmatter describes the workflow permissions, schedule, network access, and safe outputs; the body tells the agent what to do.</p>



<p class="wp-block-paragraph">For this digest, the instructions are intentionally specific:</p>



<ul class="wp-block-list">
<li>read seven approved RSS feeds;</li>



<li>look back over the most recent 14 days;</li>



<li>select 15 useful stories;</li>



<li>include at least seven GitHub Changelog stories when available;</li>



<li>cap the number of stories from every other source;</li>



<li>write a TL;DR and a “Why it matters” explanation for every selected story;</li>



<li>produce a separate five-item GitHub-only highlights section;</li>



<li>render a complete static page at <code>docs/index.html</code>.</li>
</ul>



<p class="wp-block-paragraph">This is an important detail about agentic workflows: the prompt is not decoration.</p>



<p class="wp-block-paragraph">The prompt is the product specification.</p>



<p class="wp-block-paragraph">If I only write “find interesting AI news,” I get a very creative interpretation of interesting. If I write the sources, time window, allocation rules, output shape, and quality bar, I get something I can review.</p>



<p class="wp-block-paragraph">Not magic. Just better constraints.</p>



<h2 class="wp-block-heading">From seven feeds to one readable page<a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/2026-07-10-how-agentic-workflow-works.md#from-seven-feeds-to-one-readable-page"></a></h2>



<p class="wp-block-paragraph">The workflow begins with a small set of sources:</p>



<ul class="wp-block-list">
<li>GitHub Changelog</li>



<li>TechCrunch AI</li>



<li>MIT Technology Review</li>



<li>Hacker News</li>



<li>Ars Technica</li>



<li>The Verge</li>



<li>VentureBeat AI</li>
</ul>



<p class="wp-block-paragraph">The agent extracts the title, URL, source, publication date, and plain-text excerpt for each recent entry. Then it curates the list.</p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/assets/agentic-workflow-sources.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/weekly-ai-news-digest/raw/main/docs/blog/assets/agentic-workflow-sources.png" alt="Generated illustration of multiple news sources flowing into a curated developer digest." /></a></figure>



<p class="wp-block-paragraph"><em>Generated illustration. It represents the source-to-digest concept and is not a screenshot of the product.</em></p>



<p class="wp-block-paragraph">The curation rules deliberately make GitHub the default lens. At least seven of the 15 selected stories come from GitHub Changelog when that many are available. Other sources cannot dominate the page.</p>



<p class="wp-block-paragraph">That does not mean “ignore the rest of the industry.” It means that, when I open this page, I should quickly see the developer-platform changes that may affect my daily work.</p>



<p class="wp-block-paragraph">The generated page also includes a five-bullet&nbsp;<strong>TL;DR — GitHub highlights</strong>&nbsp;block at the top. Those bullets are only allowed to use GitHub sources. This is a small rule, but it prevents a common AI-summary problem: mixing unrelated sources into a summary that sounds coherent but loses provenance.</p>



<h2 class="wp-block-heading">The UI has opinions too<a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/2026-07-10-how-agentic-workflow-works.md#the-ui-has-opinions-too"></a></h2>



<p class="wp-block-paragraph">The generated site is a self-contained HTML page. No framework, no CDN, no build pipeline that needs a different build pipeline to explain the first build pipeline.</p>



<p class="wp-block-paragraph">It uses a GitHub-inspired dark theme, with system, light, and dark modes. It also has:</p>



<ul class="wp-block-list">
<li>source filter chips;</li>



<li>tag filters;</li>



<li>full-text search across titles, sources, summaries, impact notes, and tags;</li>



<li>a live “Showing X of 15 stories” counter;</li>



<li>responsive cards for reading on a desktop or a phone.</li>
</ul>



<p class="wp-block-paragraph">The source filter starts with GitHub-related sources selected.</p>



<p class="wp-block-paragraph">Again, that is intentional. A visitor can expand the view, but the default answer to “what changed this week?” starts close to the tools many developers already use.</p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/assets/screenshots/digest-default-github-filter.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/weekly-ai-news-digest/raw/main/docs/blog/assets/screenshots/digest-default-github-filter.png" alt="Product screenshot of the generated weekly digest in dark mode with its GitHub-first source filter." /></a></figure>



<p class="wp-block-paragraph"><em>Product screenshot captured locally on July 10, 2026 at&nbsp;<code>http://127.0.0.1:4173/</code>. It shows the generated digest with the default GitHub-first filter.</em></p>



<p class="wp-block-paragraph">Small UX decisions like this are useful because they turn a generated page into something that feels curated. The agent can produce information. The product still needs an opinion about how people should consume it.</p>



<h2 class="wp-block-heading">The agent does not get to write directly to <code>main</code><a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/2026-07-10-how-agentic-workflow-works.md#the-agent-does-not-get-to-write-directly-to-main"></a></h2>



<p class="wp-block-paragraph">This is the part I care about most.</p>



<p class="wp-block-paragraph">The workflow does not hand an agent unrestricted repository write access and hope everybody has a quiet afternoon.</p>



<p class="wp-block-paragraph">The agent runs with read-only repository permissions in a sandboxed environment. It can research the approved network locations and prepare output, but the actual repository write is handled by a separate&nbsp;<code>safe-outputs</code>&nbsp;pull-request step.</p>



<p class="wp-block-paragraph">The path looks like this:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; first-line: 1; gutter: false; title: ; notranslate">
Agent (read-only, sandboxed)
  -&gt; agent output artifact
  -&gt; threat detection
  -&gt; safe-outputs pull request
  -&gt; review / auto-merge rule
  -&gt; GitHub Pages deployment

</pre></div>


<p class="wp-block-paragraph">The workflow creates a pull request with the&nbsp;<code>digest</code>&nbsp;and&nbsp;<code>automated</code>&nbsp;labels. A separate automation can merge that trusted digest PR. GitHub Pages then serves the&nbsp;<code>docs/</code>&nbsp;folder from&nbsp;<code>main</code>.</p>



<p class="wp-block-paragraph">This is a much better model than “an AI wrote directly to production.”</p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/assets/agentic-workflow-safety-path.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/weekly-ai-news-digest/raw/main/docs/blog/assets/agentic-workflow-safety-path.png" alt="Generated illustration of a secure agent workflow progressing through a safety checkpoint and pull request review to a deployed site." /></a></figure>



<p class="wp-block-paragraph"><em>Generated illustration. It represents the safety and delivery model and is not a screenshot of the product.</em></p>



<p class="wp-block-paragraph">The agent is useful where it has an advantage: reading, comparing, summarizing, and drafting. The platform keeps the repository mutation in a narrow, auditable path.</p>



<p class="wp-block-paragraph">That is how I want agentic automation to work: capable by default, but not casually omnipotent.</p>



<h2 class="wp-block-heading">Why use an agent instead of a normal script?<a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/2026-07-10-how-agentic-workflow-works.md#why-use-an-agent-instead-of-a-normal-script"></a></h2>



<p class="wp-block-paragraph">Traditional scripts are still great.</p>



<p class="wp-block-paragraph">If the task is “parse this XML,” “rename these files,” or “run these tests,” I want deterministic code. A script is faster, cheaper, easier to test, and far less likely to develop a sudden interest in the history of computer science.</p>



<p class="wp-block-paragraph">But curation is different.</p>



<p class="wp-block-paragraph">Choosing the 15 stories that matter from a large group of feeds is not only filtering by date or keyword. It involves relevance, duplication, developer impact, and context. Writing a concise explanation of why a release matters also benefits from language and reasoning.</p>



<p class="wp-block-paragraph">That is a reasonable place to use an agent.</p>



<p class="wp-block-paragraph">The pattern I am using is simple:</p>



<ul class="wp-block-list">
<li>use code for deterministic work;</li>



<li>use an agent for judgment and language;</li>



<li>put the agent behind clear boundaries;</li>



<li>make the output easy to inspect;</li>



<li>keep the final delivery path controlled by the platform.</li>
</ul>



<p class="wp-block-paragraph">You do not need an agent for every cron job. You need one when the workflow has a real reasoning step and you can state the expected output clearly.</p>



<h2 class="wp-block-heading">Things I learned while building it<a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/2026-07-10-how-agentic-workflow-works.md#things-i-learned-while-building-it"></a></h2>



<h3 class="wp-block-heading">Constraints improve the results<a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/2026-07-10-how-agentic-workflow-works.md#constraints-improve-the-results"></a></h3>



<p class="wp-block-paragraph">The most useful instructions were not the poetic ones. They were the measurable ones:</p>



<ul class="wp-block-list">
<li>14-day lookback;</li>



<li>15 stories;</li>



<li>at least seven GitHub items;</li>



<li>at most four items from any other source;</li>



<li>exactly five GitHub-only top highlights;</li>



<li>specific fields for each card.</li>
</ul>



<p class="wp-block-paragraph">An agent with a vague task will fill the ambiguity. An agent with a clear task will spend more time doing useful work.</p>



<h3 class="wp-block-heading">Generated HTML can still be a product<a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/2026-07-10-how-agentic-workflow-works.md#generated-html-can-still-be-a-product"></a></h3>



<p class="wp-block-paragraph">The page is generated, but it is not a random document dump. The template has visual rules, responsive behavior, accessibility-friendly labels, theme persistence, and filters.</p>



<p class="wp-block-paragraph">Static HTML is still one of the most reliable deployment targets on the internet.</p>



<h3 class="wp-block-heading">A pull request is a great integration boundary<a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/2026-07-10-how-agentic-workflow-works.md#a-pull-request-is-a-great-integration-boundary"></a></h3>



<p class="wp-block-paragraph">The pull request gives me a diff, history, labels, checks, and a place to stop the automation if something looks wrong.</p>



<p class="wp-block-paragraph">It is also familiar. You do not need a new dashboard to understand what changed. You can use the collaboration system that is already part of your development workflow.</p>



<h2 class="wp-block-heading">What I want to add next<a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/2026-07-10-how-agentic-workflow-works.md#what-i-want-to-add-next"></a></h2>



<p class="wp-block-paragraph">The digest is intentionally small, but there are many directions to explore:</p>



<ul class="wp-block-list">
<li>better duplicate detection across syndications;</li>



<li>recurring-topic summaries;</li>



<li>a history page for comparing weeks;</li>



<li>optional user-selected source profiles;</li>



<li>richer visual explanations of the workflow;</li>



<li>screenshots that document the actual filtering and theme experience.</li>
</ul>



<p class="wp-block-paragraph">For now, the goal is simpler: open one page, catch up on useful news, and understand why it matters.</p>



<p class="wp-block-paragraph">The project is open source on GitHub: <a href="https://github.com/elbruno/weekly-ai-news-digest" target="_blank" rel="noopener">elbruno/weekly-ai-news-digest</a>.</p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/weekly-ai-news-digest/blob/main/docs/blog/assets/screenshots/github-changelog.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/weekly-ai-news-digest/raw/main/docs/blog/assets/screenshots/github-changelog.png" alt="GitHub Changelog page used as an approved source for the digest." /></a></figure>



<p class="wp-block-paragraph"><em>Source screenshot captured from <a href="https://github.blog/changelog/" target="_blank" rel="noopener">GitHub Changelog</a> on July 10, 2026. It documents an input source, not a feature of this project.</em></p>



<p class="wp-block-paragraph">If you build your own agentic workflow, my recommendation is not “give the agent all the permissions.”</p>



<p class="wp-block-paragraph">Start with a narrow problem. Describe the output. Keep the boundary visible. Let the agent do the part where it is good.</p>



<p class="wp-block-paragraph">And let your pull request do the part where you are good.</p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/07/10/i-built-a-weekly-ai-news-digest-with-github-agentic-workflows-here-is-how-it-works/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40769</post-id>
		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/weekly-ai-news-digest/raw/main/docs/blog/assets/agentic-workflow-hero.png">
			<media:title type="html">Generated illustration of an AI-news workflow moving through a review checkpoint to a web digest.</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/weekly-ai-news-digest/raw/main/docs/blog/assets/agentic-workflow-sources.png">
			<media:title type="html">Generated illustration of multiple news sources flowing into a curated developer digest.</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/weekly-ai-news-digest/raw/main/docs/blog/assets/screenshots/digest-default-github-filter.png">
			<media:title type="html">Product screenshot of the generated weekly digest in dark mode with its GitHub-first source filter.</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/weekly-ai-news-digest/raw/main/docs/blog/assets/agentic-workflow-safety-path.png">
			<media:title type="html">Generated illustration of a secure agent workflow progressing through a safety checkpoint and pull request review to a deployed site.</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/weekly-ai-news-digest/raw/main/docs/blog/assets/screenshots/github-changelog.png">
			<media:title type="html">GitHub Changelog page used as an approved source for the digest.</media:title>
		</media:content>
	</item>
		<item>
		<title>Foundry Local Monitor — See every local AI model your machine is running with Foundry Local</title>
		<link>https://elbruno.com/2026/07/06/foundry-local-monitor-see-every-local-ai-model-your-machine-is-running-with-foundry-local/</link>
					<comments>https://elbruno.com/2026/07/06/foundry-local-monitor-see-every-local-ai-model-your-machine-is-running-with-foundry-local/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Mon, 06 Jul 2026 17:27:36 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[dotnet tool]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Foundry Local]]></category>
		<category><![CDATA[NuGet]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40764</guid>

					<description><![CDATA[Hi! If you have been playing with&#160;Foundry Local&#160;to run AI models on your own machine, you probably ran into the same thing I did:&#160;you never really know what is running. You load a model from the CLI. Your C# app loads another one through the SDK. A Python script spins up its own. A .NET [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.FoundryLocalMonitor/blob/main/docs/blog/images/foundry-local-monitor-hero.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.FoundryLocalMonitor/raw/main/docs/blog/images/foundry-local-monitor-hero.png" alt="Foundry Local Monitor hero image" /></a></figure>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"></blockquote>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">If you have been playing with&nbsp;<a href="https://learn.microsoft.com/azure/ai-foundry/foundry-local/">Foundry Local</a>&nbsp;to run AI models on your own machine, you probably ran into the same thing I did:&nbsp;<strong>you never really know what is running.</strong></p>



<p class="wp-block-paragraph">You load a model from the CLI. Your C# app loads another one through the SDK. A Python script spins up its own. A .NET Aspire sample launches a proxy. And five minutes later you have several models loaded across several ports, eating memory, and you have no idea which one belongs to what.</p>



<p class="wp-block-paragraph">Foundry Local does not run on a single fixed port, and the&nbsp;<code>foundry</code>&nbsp;CLI only sees what the CLI itself started. Everything an app loads directly through the SDK is, basically, invisible.</p>



<p class="wp-block-paragraph">So I built a small tool to fix that:&nbsp;<strong>Foundry Local Monitor</strong>.</p>



<p class="wp-block-paragraph">It is a tiny Windows tray app that watches your machine and tells you, in real time, which Foundry Local instances are running, which models are loaded, and on which device (CPU, GPU, CUDA, DirectML…). When a model loads or unloads — from&nbsp;<em>any</em>&nbsp;app — you get a notification.</p>



<h2 class="wp-block-heading">What it does<a href="https://github.com/elbruno/ElBruno.FoundryLocalMonitor/blob/main/docs/blog/foundry-local-monitor.md#what-it-does"></a></h2>



<ul class="wp-block-list">
<li>Lives in your system tray, out of the way.</li>



<li>Discovers <strong>all</strong> Foundry Local instances on your machine, no matter which language or SDK started them.</li>



<li>Shows the models loaded by each instance, grouped by process.</li>



<li>Tells you the device backend for each model (CPU / GPU / CUDA / TensorRT / DirectML / WinML) with a colored badge.</li>



<li>Fires a toast notification when a model is loaded or unloaded.</li>



<li>Has a handy <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4c2.png" alt="📂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> button to open the process location on disk.</li>
</ul>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.FoundryLocalMonitor/blob/main/docs/blog/images/loaded-models-tab.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.FoundryLocalMonitor/raw/main/docs/blog/images/loaded-models-tab.png" alt="Loaded Models tab" /></a></figure>



<h2 class="wp-block-heading">Install it<a href="https://github.com/elbruno/ElBruno.FoundryLocalMonitor/blob/main/docs/blog/foundry-local-monitor.md#install-it"></a></h2>



<p class="wp-block-paragraph">It is published as a .NET global tool, so one line and you are done:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet tool install -g ElBruno.FoundryLocalMonitor</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then run it:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">foundrylocalmon</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That&#8217;s it. Look for the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> icon in your system tray. Right-click for options, double-click to open the main window with the Status, Loaded Models, and Available Models tabs.</p>



<p class="wp-block-paragraph">To update later:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet tool update -g ElBruno.FoundryLocalMonitor</div></code></pre>
		</div>
	</div>
</div>


<h2 class="wp-block-heading">A quick example: the scenario it detects<a href="https://github.com/elbruno/ElBruno.FoundryLocalMonitor/blob/main/docs/blog/foundry-local-monitor.md#a-quick-example-the-scenario-it-detects"></a></h2>



<p class="wp-block-paragraph">Here is the kind of code the monitor is designed to see. This is a plain .NET console app using the&nbsp;<a href="https://www.nuget.org/packages/Microsoft.AI.Foundry.Local"><code>Microsoft.AI.Foundry.Local</code></a>&nbsp;SDK — no CLI involved. The SDK spins up its own OpenAI-compatible REST server, and&nbsp;<em>that</em>&nbsp;is exactly what the monitor finds and shows in real time.</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">Microsoft</span>.<span class="tok-variableName">AI</span>.<span class="tok-variableName">Foundry</span>.<span class="tok-variableName">Local</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">Microsoft</span>.<span class="tok-variableName">Extensions</span>.<span class="tok-variableName">Logging</span>.<span class="tok-variableName">Abstractions</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-comment">// The SDK hosts an internal OpenAI-compatible server on this address.</span></div><div class="cm-line"><span class="tok-comment">// The C# SDK default is 55588 (Python uses 55589).</span></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">config</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">Configuration</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">AppName</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;MyFoundryApp&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">Web</span>     <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">Configuration</span>.<span class="tok-variableName">WebService</span> { <span class="tok-variableName">Urls</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;http://127.0.0.1:55588&quot;</span> }</div><div class="cm-line">};</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">await</span> <span class="tok-variableName">FoundryLocalManager</span>.<span class="tok-variableName">CreateAsync</span>(<span class="tok-variableName">config</span>, <span class="tok-variableName">NullLogger</span>.<span class="tok-variableName">Instance</span>);</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">manager</span> <span class="tok-operator">=</span> <span class="tok-variableName">FoundryLocalManager</span>.<span class="tok-variableName">Instance</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-comment">// Start the web service — this is the endpoint the monitor discovers.</span></div><div class="cm-line"><span class="tok-keyword">await</span> <span class="tok-variableName">manager</span>.<span class="tok-variableName">StartWebServiceAsync</span>();</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">endpoint</span> <span class="tok-operator">=</span> <span class="tok-variableName">manager</span>.<span class="tok-variableName">Urls</span><span class="tok-operator">?</span>.<span class="tok-variableName">FirstOrDefault</span>();</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Foundry Local is serving at {endpoint}&quot;</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">From this point on, the app is completely&nbsp;<strong>invisible to the&nbsp;<code>foundry</code>&nbsp;CLI</strong>&nbsp;— but the monitor sees it, because it answers on an HTTP port. The moment a model loads, you get a toast.</p>



<p class="wp-block-paragraph">Talking to that endpoint is just OpenAI-compatible HTTP, so any .NET HttpClient works:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">System</span>.<span class="tok-variableName">Net</span>.<span class="tok-variableName">Http</span>.<span class="tok-variableName">Json</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">http</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">HttpClient</span> { <span class="tok-variableName">BaseAddress</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">Uri</span>(<span class="tok-variableName">endpoint</span><span class="tok-operator">!</span>) };</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">response</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">http</span>.<span class="tok-variableName">PostAsJsonAsync</span>(<span class="tok-string">&quot;/v1/chat/completions&quot;</span>, <span class="tok-keyword">new</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">model</span>    <span class="tok-operator">=</span> <span class="tok-string">&quot;qwen2.5-coder-0.5b&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">messages</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span>[]</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">new</span> { <span class="tok-variableName">role</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;system&quot;</span>, <span class="tok-variableName">content</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;You are a helpful assistant.&quot;</span> },</div><div class="cm-line">        <span class="tok-keyword">new</span> { <span class="tok-variableName">role</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;user&quot;</span>,   <span class="tok-variableName">content</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;What is Foundry Local in one sentence?&quot;</span> }</div><div class="cm-line">    },</div><div class="cm-line">    <span class="tok-variableName">max_tokens</span> <span class="tok-operator">=</span> <span class="tok-number">200</span></div><div class="cm-line">});</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">json</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">response</span>.<span class="tok-variableName">Content</span>.<span class="tok-variableName">ReadAsStringAsync</span>();</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">json</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">You can find a full end-to-end sample (init → start → load → chat → unload → stop) in the&nbsp;<a href="https://github.com/elbruno/ElBruno.FoundryLocalMonitor/tree/main/samples/FoundryLocalChat"><code>samples/FoundryLocalChat</code></a>&nbsp;folder of the repo.</p>



<h2 class="wp-block-heading">How instances are discovered<a href="https://github.com/elbruno/ElBruno.FoundryLocalMonitor/blob/main/docs/blog/foundry-local-monitor.md#how-instances-are-discovered"></a></h2>



<p class="wp-block-paragraph">This is the part I like the most, so let me explain the trick.</p>



<p class="wp-block-paragraph">Foundry Local does not use a well-known port. The daemon picks a random one at startup, the C# SDK defaults to&nbsp;<code>55588</code>, the Python SDK to&nbsp;<code>55589</code>, and an Aspire proxy gets whatever Aspire assigns. There is no central registry to ask.</p>



<p class="wp-block-paragraph">So the monitor does something simple and effective: it&nbsp;<strong>scans your local ports</strong>.</p>



<ol class="wp-block-list">
<li>It asks the OS for every TCP port currently listening on <code>127.0.0.1</code>. This is a fast kernel call, no guessing.</li>



<li>For each of those ports — <strong>in parallel</strong> — it sends a <code>GET /v1/models</code> request.</li>



<li>Any port that answers with <code>{"object":"list"}</code> is a Foundry Local (OpenAI-compatible) endpoint. Everything else is ignored.</li>



<li>Each discovered endpoint is enriched with process info: name, PID, full path, and whether it is an SDK proxy or the daemon.</li>



<li>Endpoints are grouped by PID, so one process = one card in the UI.</li>
</ol>



<p class="wp-block-paragraph">Why parallel? A typical machine has 15–30 listening ports. Probing them one by one, with a timeout each, could take 20+ seconds. Fanning out all the probes at once brings the whole scan down to roughly the time of a single probe — about 800ms. The scan re-runs every 30 seconds, so newly launched apps show up on their own.</p>



<p class="wp-block-paragraph">In .NET, the port enumeration and the parallel probe boil down to something like this:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">System</span>.<span class="tok-variableName">Net</span>.<span class="tok-variableName">NetworkInformation</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-comment">// 1. Ask the OS for every port listening on localhost (fast kernel call).</span></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">listeners</span> <span class="tok-operator">=</span> <span class="tok-variableName">IPGlobalProperties</span>.<span class="tok-variableName">GetIPGlobalProperties</span>()</div><div class="cm-line">    .<span class="tok-variableName">GetActiveTcpListeners</span>()</div><div class="cm-line">    .<span class="tok-variableName">Where</span>(<span class="tok-variableName">ep</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">IPAddress</span>.<span class="tok-variableName">IsLoopback</span>(<span class="tok-variableName">ep</span>.<span class="tok-variableName">Address</span>))</div><div class="cm-line">    .<span class="tok-variableName">Select</span>(<span class="tok-variableName">ep</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">ep</span>.<span class="tok-variableName">Port</span>)</div><div class="cm-line">    .<span class="tok-variableName">Distinct</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-comment">// 2. Probe them all in parallel — a Foundry endpoint answers /v1/models</span></div><div class="cm-line"><span class="tok-comment">//    with {&quot;object&quot;:&quot;list&quot;}.</span></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">probes</span> <span class="tok-operator">=</span> <span class="tok-variableName">listeners</span>.<span class="tok-variableName">Select</span>(<span class="tok-keyword">async</span> <span class="tok-variableName">port</span> <span class="tok-operator">=&gt;</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">http</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">HttpClient</span> { <span class="tok-variableName">Timeout</span> <span class="tok-operator">=</span> <span class="tok-typeName">TimeSpan</span>.<span class="tok-variableName">FromMilliseconds</span>(<span class="tok-number">800</span>) };</div><div class="cm-line">    <span class="tok-keyword">try</span></div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">json</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">http</span>.<span class="tok-variableName">GetStringAsync</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;http://127.0.0.1:{port}/v1/models&quot;</span>);</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">json</span>.<span class="tok-variableName">Contains</span>(<span class="tok-string">&quot;\&quot;object\&quot;:\&quot;list\&quot;&quot;</span>) <span class="tok-operator">?</span> <span class="tok-variableName">port</span> : (<span class="tok-typeName">int</span><span class="tok-operator">?</span>)<span class="tok-atom">null</span>;</div><div class="cm-line">    }</div><div class="cm-line">    <span class="tok-keyword">catch</span> { <span class="tok-keyword">return</span> <span class="tok-atom">null</span>; }</div><div class="cm-line">});</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">foundryPorts</span> <span class="tok-operator">=</span> (<span class="tok-keyword">await</span> <span class="tok-typeName">Task</span>.<span class="tok-variableName">WhenAll</span>(<span class="tok-variableName">probes</span>))</div><div class="cm-line">    .<span class="tok-variableName">Where</span>(<span class="tok-variableName">p</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">p</span> <span class="tok-keyword">is</span> <span class="tok-variableName">not</span> <span class="tok-atom">null</span>)</div><div class="cm-line">    .<span class="tok-variableName">Select</span>(<span class="tok-variableName">p</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">p</span><span class="tok-operator">!</span>.<span class="tok-variableName">Value</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The nice side effect: because the monitor talks HTTP and not the CLI, it sees&nbsp;<strong>everything</strong>&nbsp;— CLI, C# SDK, Python SDK, Aspire, or any OpenAI-compatible local server. If it listens and it answers, it shows up.</p>



<h2 class="wp-block-heading">How models are discovered<a href="https://github.com/elbruno/ElBruno.FoundryLocalMonitor/blob/main/docs/blog/foundry-local-monitor.md#how-models-are-discovered"></a></h2>



<p class="wp-block-paragraph">Once the monitor knows where the endpoints are, it asks each one what it is serving:</p>



<ul class="wp-block-list">
<li>For an <strong>SDK proxy</strong>, it calls <code>GET /v1/models</code>.</li>



<li>For the <strong>daemon</strong>, it calls <code>GET /openai/loadedmodels</code> (the models actually in memory).</li>
</ul>



<p class="wp-block-paragraph">The interesting bit is the device. Foundry Local encodes the backend right in the model id as a suffix, for example:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Phi-4-mini-instruct-cuda-gpu:5</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The monitor strips the version (<code>:5</code>), reads the suffix, and turns it into a friendly badge:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Suffix</th><th>Device</th><th>Badge</th></tr></thead><tbody><tr><td><code>-cuda-gpu</code></td><td>CUDA</td><td><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f7e2.png" alt="🟢" class="wp-smiley" style="height: 1em; max-height: 1em;" /> green</td></tr><tr><td><code>-trtrtx-gpu</code></td><td>TensorRT</td><td><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f7e9.png" alt="🟩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> emerald</td></tr><tr><td><code>-gpu</code></td><td>GPU</td><td><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f7e2.png" alt="🟢" class="wp-smiley" style="height: 1em; max-height: 1em;" /> green</td></tr><tr><td><code>-cpu</code></td><td>CPU</td><td><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f535.png" alt="🔵" class="wp-smiley" style="height: 1em; max-height: 1em;" /> blue</td></tr><tr><td><code>-directml-gpu</code></td><td>DirectML</td><td><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f7e3.png" alt="🟣" class="wp-smiley" style="height: 1em; max-height: 1em;" /> purple</td></tr><tr><td><code>-winml-cpu</code></td><td>WinML</td><td><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f7e0.png" alt="🟠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> orange</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The parsing itself is a nice little C# pattern-match — strip the version, match the longest device suffix, map it to a label:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">static</span> (<span class="tok-typeName">string</span> <span class="tok-variableName">Alias</span>, <span class="tok-typeName">string</span> <span class="tok-variableName">Device</span>) <span class="tok-variableName">ParseModelId</span>(<span class="tok-typeName">string</span> <span class="tok-variableName">modelId</span>)</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-comment">// &quot;Phi-4-mini-instruct-cuda-gpu:5&quot; -&gt; drop the &quot;:5&quot; version.</span></div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">noVersion</span> <span class="tok-operator">=</span> <span class="tok-variableName">modelId</span>.<span class="tok-variableName">Split</span>(<span class="tok-string">&apos;:&apos;</span>)[<span class="tok-number">0</span>];</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-comment">// Longest suffixes first so &quot;-cuda-gpu&quot; wins over &quot;-gpu&quot;.</span></div><div class="cm-line">    <span class="tok-typeName">string</span>[] <span class="tok-variableName">suffixes</span> <span class="tok-operator">=</span></div><div class="cm-line">    [</div><div class="cm-line">        <span class="tok-string">&quot;-trtrtx-gpu&quot;</span>, <span class="tok-string">&quot;-cuda-gpu&quot;</span>, <span class="tok-string">&quot;-generic-gpu&quot;</span>, <span class="tok-string">&quot;-generic-cpu&quot;</span>,</div><div class="cm-line">        <span class="tok-string">&quot;-winml-directml&quot;</span>, <span class="tok-string">&quot;-winml-cpu&quot;</span>, <span class="tok-string">&quot;-directml-gpu&quot;</span>, <span class="tok-string">&quot;-cpu&quot;</span>, <span class="tok-string">&quot;-gpu&quot;</span></div><div class="cm-line">    ];</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">foreach</span> (<span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">suffix</span> <span class="tok-keyword">in</span> <span class="tok-variableName">suffixes</span>)</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">if</span> (<span class="tok-operator">!</span><span class="tok-variableName">noVersion</span>.<span class="tok-variableName">EndsWith</span>(<span class="tok-variableName">suffix</span>, <span class="tok-variableName">StringComparison</span>.<span class="tok-variableName">OrdinalIgnoreCase</span>))</div><div class="cm-line">            <span class="tok-keyword">continue</span>;</div><div class="cm-line"></div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-keyword">alias</span>  <span class="tok-operator">=</span> <span class="tok-variableName">noVersion</span>[..<span class="tok-variableName">^suffix</span>.<span class="tok-variableName">Length</span>];</div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">device</span> <span class="tok-operator">=</span> <span class="tok-variableName">suffix</span> <span class="tok-keyword">switch</span></div><div class="cm-line">        {</div><div class="cm-line">            <span class="tok-string">&quot;-trtrtx-gpu&quot;</span>                       <span class="tok-operator">=&gt;</span> <span class="tok-string">&quot;TensorRT&quot;</span>,</div><div class="cm-line">            <span class="tok-string">&quot;-cuda-gpu&quot;</span>                         <span class="tok-operator">=&gt;</span> <span class="tok-string">&quot;CUDA&quot;</span>,</div><div class="cm-line">            <span class="tok-string">&quot;-generic-gpu&quot;</span> <span class="tok-variableName">or</span> <span class="tok-string">&quot;-gpu&quot;</span>            <span class="tok-operator">=&gt;</span> <span class="tok-string">&quot;GPU&quot;</span>,</div><div class="cm-line">            <span class="tok-string">&quot;-generic-cpu&quot;</span> <span class="tok-variableName">or</span> <span class="tok-string">&quot;-cpu&quot;</span>            <span class="tok-operator">=&gt;</span> <span class="tok-string">&quot;CPU&quot;</span>,</div><div class="cm-line">            <span class="tok-string">&quot;-winml-directml&quot;</span> <span class="tok-variableName">or</span> <span class="tok-string">&quot;-directml-gpu&quot;</span><span class="tok-operator">=&gt;</span> <span class="tok-string">&quot;DirectML&quot;</span>,</div><div class="cm-line">            <span class="tok-string">&quot;-winml-cpu&quot;</span>                        <span class="tok-operator">=&gt;</span> <span class="tok-string">&quot;WinML&quot;</span>,</div><div class="cm-line">            <span class="tok-variableName">_</span>                                   <span class="tok-operator">=&gt;</span> <span class="tok-string">&quot;?&quot;</span></div><div class="cm-line">        };</div><div class="cm-line">        <span class="tok-keyword">return</span> (<span class="tok-keyword">alias</span>, <span class="tok-variableName">device</span>);</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">return</span> (<span class="tok-variableName">noVersion</span>, <span class="tok-string">&quot;?&quot;</span>); <span class="tok-comment">// utility models have no device suffix</span></div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">So at a glance you can tell whether that model is actually running on your GPU or quietly sitting on the CPU. Very useful when you are wondering why something is slow.</p>



<p class="wp-block-paragraph">And about those notifications: to avoid noise, models discovered on the SDK&#8217;s own internal ports are marked&nbsp;<em>silent</em>&nbsp;by default. Only load/unload events from real apps or the daemon pop a toast. You can tune this in Settings.</p>



<h2 class="wp-block-heading">Wrap up<a href="https://github.com/elbruno/ElBruno.FoundryLocalMonitor/blob/main/docs/blog/foundry-local-monitor.md#wrap-up"></a></h2>



<p class="wp-block-paragraph">Foundry Local is a fantastic way to run models locally, and this little monitor gives you the missing piece of visibility:&nbsp;<strong>what is actually running, right now, and where.</strong>&nbsp;Install it, forget about it, and let it tell you when things change.</p>



<p class="wp-block-paragraph">If you find a bug or want a feature, the repo is open — issues and PRs welcome.</p>



<h2 class="wp-block-heading">Resources<a href="https://github.com/elbruno/ElBruno.FoundryLocalMonitor/blob/main/docs/blog/foundry-local-monitor.md#resources"></a></h2>



<ul class="wp-block-list">
<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4e6.png" alt="📦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> NuGet package: <a href="https://www.nuget.org/packages/ElBruno.FoundryLocalMonitor/">ElBruno.FoundryLocalMonitor</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Foundry Local .NET SDK: <a href="https://www.nuget.org/packages/Microsoft.AI.Foundry.Local">Microsoft.AI.Foundry.Local</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4bb.png" alt="💻" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Source code on GitHub: <a href="https://github.com/elbruno/ElBruno.FoundryLocalMonitor">elbruno/ElBruno.FoundryLocalMonitor</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4d6.png" alt="📖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Foundry Local documentation: <a href="https://learn.microsoft.com/azure/ai-foundry/foundry-local/">learn.microsoft.com/azure/ai-foundry/foundry-local</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Get started with Foundry Local: <a href="https://learn.microsoft.com/azure/ai-foundry/foundry-local/get-started">Foundry Local quickstart</a></li>
</ul>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/07/06/foundry-local-monitor-see-every-local-ai-model-your-machine-is-running-with-foundry-local/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40764</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/07/foundry-local-monitor-hero-small.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/07/foundry-local-monitor-hero-small.png">
			<media:title type="html">foundry-local-monitor-hero-small</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.FoundryLocalMonitor/raw/main/docs/blog/images/foundry-local-monitor-hero.png">
			<media:title type="html">Foundry Local Monitor hero image</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.FoundryLocalMonitor/raw/main/docs/blog/images/loaded-models-tab.png">
			<media:title type="html">Loaded Models tab</media:title>
		</media:content>
	</item>
		<item>
		<title>Run the Right AI Model for the Right Copilot Task — Local or Cloud!</title>
		<link>https://elbruno.com/2026/07/04/run-the-right-ai-model-for-the-right-copilot-task-no-cloud-credits-wasted/</link>
					<comments>https://elbruno.com/2026/07/04/run-the-right-ai-model-for-the-right-copilot-task-no-cloud-credits-wasted/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Sat, 04 Jul 2026 20:33:09 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[BYOK]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[GitHub Copilot]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40756</guid>

					<description><![CDATA[Hi! Part 1 of the CopilotHarness series Big models are great for heavy thinking. But what about simple questions — &#8220;rename this variable&#8221;, &#8220;write a short docstring&#8221;, &#8220;what does this function do&#8221;? Those don&#8217;t need GPT-5 across the internet. They can be answered instantly by a model running on your own machine, offline, for free. [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph"><em>Part 1 of the CopilotHarness series</em></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness-images/hero.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/hero.png" alt="Hero: local AI running on your machine" /></a></figure>



<p class="wp-block-paragraph">Big models are great for heavy thinking. But what about simple questions — &#8220;rename this variable&#8221;, &#8220;write a short docstring&#8221;, &#8220;what does this function do&#8221;? Those don&#8217;t need GPT-5 across the internet. They can be answered instantly by a model running on your own machine, offline, for free.</p>



<p class="wp-block-paragraph">This post shows you how to wire GitHub Copilot in VS Code to local models using three minimal proxies — one for Ollama, one for Foundry Local, one for Azure OpenAI — and how to run all three together with a single command.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">1. How BYOK Works in VS Code</h2>



<p class="wp-block-paragraph">VS Code Copilot supports a&nbsp;<strong>Bring Your Own Model</strong>&nbsp;mechanism. You register any OpenAI-compatible endpoint in a config file, and Copilot treats it as just another model in the picker — no extension, no plugin.</p>



<p class="wp-block-paragraph"><strong>Official docs:</strong>&nbsp;<a href="https://code.visualstudio.com/docs/copilot/language-models">Bring your own model to GitHub Copilot Chat</a></p>



<p class="wp-block-paragraph">The config lives in a file called&nbsp;<code>chatLanguageModels.json</code>&nbsp;in your VS Code user folder:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: css; title: ; notranslate">
{
  "providers": &#91;
    {
      "name": "Ollama (local)",
      "vendor": "customendpoint",
      "url": "http://localhost:5099/v1",
      "modelId": "llama3.1:8b",
      "chatModelId": "copilot-chat-model"
    }
  ]
}
</pre></div>


<pre class="wp-block-preformatted">That's it — point <code>url</code> at any OpenAI-compatible endpoint, give it a name, and it appears in Copilot's model picker.</pre>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness-images/byok-flow.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/byok-flow.png" alt="BYOK flow: VS Code → config → proxy → model" /></a></figure>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">2. The Three Proxy Flavors<a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness.md#2-the-three-proxy-flavors"></a></h2>



<p class="wp-block-paragraph">A local model provider isn&#8217;t always OpenAI-compatible out of the box. Each proxy in this repo acts as a thin translation layer — it speaks OpenAI on one side and the local backend on the other.</p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness-images/proxy-overview.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/proxy-overview.png" alt="Proxy overview: three proxies connecting VS Code to different backends" /></a></figure>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Proxy</th><th>Port</th><th>Backend</th><th>Best for</th></tr></thead><tbody><tr><td><strong>OllamaProxy</strong></td><td>5099</td><td>Ollama (local)</td><td>Quickest start, huge model catalog</td></tr><tr><td><strong>FoundryLocalProxy</strong></td><td>5101</td><td>Foundry Local SDK (offline, NPU)</td><td>Offline/air-gapped, hardware acceleration</td></tr><tr><td><strong>FoundryProxy</strong></td><td>5100</td><td>Azure OpenAI / Foundry cloud</td><td>Production-grade, secret-managed cloud models</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Each proxy is a&nbsp;<strong>single ASP.NET Core Minimal API file</strong>&nbsp;— no frameworks, no abstractions. The entire proxy fits on one screen. That&#8217;s intentional: these are teaching samples, not production middleware.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">3. The Shared Secret: Unwrapping Copilot&#8217;s Envelope<a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness.md#3-the-shared-secret-unwrapping-copilots-envelope"></a></h2>



<p class="wp-block-paragraph">Here&#8217;s something most developers don&#8217;t know: when GitHub Copilot Chat sends &#8220;hi&#8221;, it doesn&#8217;t send just&nbsp;<code>"hi"</code>. It sends this:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
&lt;attachments&gt;...file contents...&lt;/attachments&gt;
&lt;context&gt;...editor state...&lt;/context&gt;
&lt;reminderInstructions&gt;...workspace instructions...&lt;/reminderInstructions&gt;
&lt;userRequest&gt;hi&lt;/userRequest&gt;
</pre></div>


<pre class="wp-block-preformatted">The actual message is buried inside <code>&lt;userRequest&gt;</code>. A proxy that naively reads the last user message sees <strong>~3 KB of boilerplate</strong> instead of the word "hi".</pre>



<p class="wp-block-paragraph">All three proxies share one class — <code>CopilotMessageExtractor</code> — that unwraps this envelope. It lives in the shared <code>Proxies.Common</code> library:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
// The key method — finds &lt;userRequest&gt;...&lt;/userRequest&gt; and returns its content.
// Falls back gracefully for non-Copilot clients (curl, SDK) that send plain text.
public static string ExtractTypedUserMessage(string rawUserMessage)
{
    // Look for &lt;userRequest&gt; first (VS Code Copilot always uses this)
    var userRequest = ExtractTagContent(rawUserMessage, &quot;userRequest&quot;)
                   ?? ExtractTagContent(rawUserMessage, &quot;user-request&quot;);

    if (!string.IsNullOrWhiteSpace(userRequest))
        return userRequest.Trim();

    // No tag — strip all known wrapper blocks and return what&#039;s left
    var stripped = rawUserMessage;
    foreach (var tag in CopilotWrapperTags)
        stripped = RemoveTagBlock(stripped, tag);

    // If stripping removed everything, fall back to the raw message
    // (this path is hit for plain curl/SDK clients — they send plain text)
    return string.IsNullOrWhiteSpace(stripped.Trim())
        ? rawUserMessage.Trim()
        : stripped.Trim();
}
</pre></div>


<p class="wp-block-paragraph">This class is why the proxies work correctly with both Copilot Chat and direct API calls. The logging in each proxy shows the <em>real</em> ask — not the 3 KB envelope.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">4. OllamaProxy — 5 Minutes to Your First Local Model<a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness.md#4-ollamaproxy--5-minutes-to-your-first-local-model"></a></h2>



<p class="wp-block-paragraph"><strong>Pre-requisite:</strong>&nbsp;<a href="https://ollama.com/">Ollama</a>&nbsp;running with at least one model pulled.</p>



<pre class="wp-block-verse"># Pull a model
ollama pull llama3.1:8b

# Start the proxy
cd src/proxies/OllamaProxy
dotnet run
# → http://localhost:5099</pre>



<p class="wp-block-paragraph">The proxy auto-discovers your installed Ollama models and passes the model ID through. Add it to VS Code:</p>



<pre class="wp-block-verse">// chatLanguageModels.json  (Windows: %APPDATA%\Code\User\)
{
  "providers": [
    {
      "name": "Ollama — llama3.1:8b",
      "vendor": "customendpoint",
      "url": "http://localhost:5099/v1",
      "modelId": "llama3.1:8b",
      "chatModelId": "copilot-chat-model"
    }
  ]
}</pre>



<p class="wp-block-paragraph">Verify it&#8217;s running:</p>



<pre class="wp-block-verse">curl http://localhost:5099/health
# → {"status":"ok","backend":"ollama","models":["llama3.1:8b",...]}</pre>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness-images/testapp-health.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/testapp-health.png" alt="Proxies test app — health dashboard showing all three proxies green" /></a></figure>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">5. FoundryLocalProxy — Offline + NPU Inference<a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness.md#5-foundrylocalproxy--offline--npu-inference"></a></h2>



<p class="wp-block-paragraph"><a href="https://github.com/microsoft/foundry-local">Microsoft Foundry Local</a>&nbsp;runs models fully offline using the ONNX Runtime with hardware acceleration (CPU, GPU, NPU on Windows).</p>



<p class="wp-block-paragraph"><strong>No pre-requisites</strong>&nbsp;— the SDK downloads the model on first run and caches it locally.</p>



<pre class="wp-block-verse">cd src/proxies/FoundryLocalProxy
dotnet run
# First run: downloads phi-4-mini (~2.5 GB) automatically
# → http://localhost:5101</pre>



<p class="wp-block-paragraph">The&nbsp;<strong>Models page</strong>&nbsp;in the test app shows which models are cached, lets you load/unload (frees GPU RAM instantly), and delete models from disk:</p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness-images/testapp-models.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/testapp-models.png" alt="Foundry Local model management — load, unload, delete cached models" /></a></figure>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" />&nbsp;<strong>Tip:</strong>&nbsp;Use the Models page to download a model&nbsp;<em>before</em>&nbsp;chatting with it. If you send a chat request to an unloaded model, you get a clear error explaining the model needs to be loaded first — not a cryptic 500.</p>
</blockquote>



<p class="wp-block-paragraph">Add it to VS Code alongside Ollama — Copilot lets you pick which model to use per conversation:</p>



<pre class="wp-block-verse">{
  "name": "Foundry Local — phi-4-mini",
  "vendor": "customendpoint",
  "url": "http://localhost:5101/v1",
  "modelId": "phi-4-mini",
  "chatModelId": "copilot-chat-model"
}</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">6. FoundryProxy — Azure OpenAI with Proper Secret Management<a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness.md#6-foundryproxy--azure-openai-with-proper-secret-management"></a></h2>



<p class="wp-block-paragraph">For cloud models, FoundryProxy uses&nbsp;<a href="https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets">.NET User Secrets</a>&nbsp;so your API key never touches the repo.</p>



<pre class="wp-block-verse">cd src/proxies/FoundryProxy

# Store credentials locally (never committed to git)
dotnet user-secrets set "Foundry:Endpoint"   "https://your-resource.openai.azure.com"
dotnet user-secrets set "Foundry:ApiKey"     "your-key"
dotnet user-secrets set "Foundry:Deployment" "gpt-4o-mini"

dotnet run
# → http://localhost:5100</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">7. All Three Together — One Command with Aspire<a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness.md#7-all-three-together--one-command-with-aspire"></a></h2>



<p class="wp-block-paragraph">The fastest way to run everything is via the&nbsp;<a href="https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview">.NET Aspire</a>&nbsp;CLI. One command starts all three proxies, the Blazor test app, and the Aspire dashboard with logs, traces, and health checks:</p>



<pre class="wp-block-verse">cd src/proxies
aspire start</pre>



<p class="wp-block-paragraph">What starts:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Service</th><th>URL</th><th>What it is</th></tr></thead><tbody><tr><td><code>ollama-proxy</code></td><td><a href="http://localhost:5099/">http://localhost:5099</a></td><td>OllamaProxy</td></tr><tr><td><code>foundry-proxy</code></td><td><a href="http://localhost:5100/">http://localhost:5100</a></td><td>FoundryProxy</td></tr><tr><td><code>foundry-local-proxy</code></td><td><a href="http://localhost:5101/">http://localhost:5101</a></td><td>FoundryLocalProxy</td></tr><tr><td><code>proxies-test-app</code></td><td><a href="http://localhost:5102/">http://localhost:5102</a></td><td>Blazor test UI</td></tr><tr><td>Aspire dashboard</td><td>printed in console</td><td>Logs, traces, health for all services</td></tr></tbody></table></figure>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Requires Aspire CLI:</strong>&nbsp;<code>dotnet workload install aspire</code></p>
</blockquote>



<p class="wp-block-paragraph">The Blazor&nbsp;<strong>test app</strong>&nbsp;at&nbsp;<code>http://localhost:5102</code>&nbsp;gives you a browser UI to test all three proxies without writing any code:</p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness-images/testapp-chat.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/testapp-chat.png" alt="Chat page — pick a proxy, model, and send a message with streaming support" /></a></figure>



<p class="wp-block-paragraph"><a target="_blank" rel="noopener" href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness-images/testapp-compare.png"><img src="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/testapp-compare.png" alt="Compare page — same prompt sent to all three proxies simultaneously" /></a>&nbsp;traces for every request, including custom&nbsp;<code>LlmActivity</code>&nbsp;spans with prompt text, model ID, token counts, and latency. You can see exactly what Copilot sent and what the model returned.</p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness-images/aspire-dashboard.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/aspire-dashboard.png" alt="Aspire dashboard — all four services running and healthy" /></a></figure>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness-images/aspire-traces.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/aspire-traces.png" alt="Aspire traces — LLM spans with latency and token counts" /></a></figure>



<p class="wp-block-paragraph">To stop everything:</p>



<pre class="wp-block-verse">aspire stop</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">8. Wire It to VS Code Copilot<a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness.md#8-wire-it-to-vs-code-copilot"></a></h2>



<p class="wp-block-paragraph">The&nbsp;<code>/setup</code>&nbsp;page at&nbsp;<code>http://localhost:5102/setup</code>&nbsp;generates the exact&nbsp;<code>chatLanguageModels.json</code>&nbsp;snippet for each running proxy, with the correct port and model ID. Copy and paste into your VS Code user config folder:</p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness-images/testapp-setup.png" target="_blank" rel="noopener"><img src="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/testapp-setup.png" alt="Setup page — auto-generated VS Code config snippets for each proxy" /></a></figure>



<ul class="wp-block-list">
<li><strong>Windows:</strong> <code>%APPDATA%\Code\User\chatLanguageModels.json</code></li>



<li><strong>macOS:</strong> <code>~/Library/Application Support/Code/User/chatLanguageModels.json</code></li>



<li><strong>Linux:</strong> <code>~/.config/Code/User/chatLanguageModels.json</code></li>
</ul>



<p class="wp-block-paragraph">After saving, reload VS Code. Open Copilot Chat, click the model picker, and your local models appear alongside the built-in cloud models.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" />&nbsp;<strong>Shortcut:</strong>&nbsp;If you have the&nbsp;<a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/src/tools/CopilotHarness.Tool/README.md">CopilotHarness CLI tool</a>&nbsp;installed, running&nbsp;<code>harness init</code>&nbsp;writes this file automatically.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">9. What&#8217;s Next — Smart Routing<a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness.md#9-whats-next--smart-routing"></a></h2>



<p class="wp-block-paragraph">The proxies shown here are static: you pick a model manually per conversation. The next level is&nbsp;<strong>automatic routing</strong>&nbsp;— where every Copilot request is analyzed and sent to the best model automatically.</p>



<p class="wp-block-paragraph"><em>&#8220;Is this a simple rename? → local llama3.1:8b. Is this a complex architecture question? → cloud GPT-5. Is this about GitHub Actions? → a specialist agent.&#8221;</em></p>



<p class="wp-block-paragraph">That&#8217;s what the full&nbsp;<a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/README.md">CopilotHarness router</a>&nbsp;does — policy-based routing with semantic matching, local classifiers, and per-request telemetry. Part 2 of this series walks through building and using it.</p>



<p class="wp-block-paragraph"><strong>Repo:</strong>&nbsp;<a href="https://github.com/elbruno/ElBruno.CopilotHarness">github.com/elbruno/ElBruno.CopilotHarness</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Quick Reference<a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness.md#quick-reference"></a></h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Goal</th><th>Command</th></tr></thead><tbody><tr><td>Start just Ollama proxy</td><td><code>cd src/proxies/OllamaProxy &amp;&amp; dotnet run</code></td></tr><tr><td>Start all three + test UI</td><td><code>cd src/proxies &amp;&amp; aspire start</code></td></tr><tr><td>Stop all</td><td><code>aspire stop</code></td></tr><tr><td>Generate VS Code config</td><td>Open&nbsp;<a href="http://localhost:5102/setup">http://localhost:5102/setup</a></td></tr><tr><td>View traces</td><td>Open Aspire dashboard URL printed in console</td></tr><tr><td>Manage Foundry Local models</td><td>Open&nbsp;<a href="http://localhost:5102/models">http://localhost:5102/models</a></td></tr><tr><td>Test proxy health</td><td><code>curl http://localhost:5099/health</code></td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<p class="wp-block-paragraph"><em>This is Part 1 of the CopilotHarness series.</em><br /><em>Next:&nbsp;<a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog">Part 2 — Smart Routing: Sending Each Request to the Right Model Automatically</a></em></p>



<p class="wp-block-paragraph"><em>Code:&nbsp;<a href="https://github.com/elbruno/ElBruno.CopilotHarness/tree/main/src/proxies">github.com/elbruno/ElBruno.CopilotHarness/tree/main/src/proxies</a></em><a href="https://github.com/elbruno/ElBruno.CopilotHarness/blob/main/docs/blog/01-CopilotHarness.md#run-the-right-ai-model-for-the-right-copilot-task--no-cloud-credits-wasted"></a></p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/07/04/run-the-right-ai-model-for-the-right-copilot-task-no-cloud-credits-wasted/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40756</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/07/hero.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/07/hero.png">
			<media:title type="html">hero</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/hero.png">
			<media:title type="html">Hero: local AI running on your machine</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/byok-flow.png">
			<media:title type="html">BYOK flow: VS Code → config → proxy → model</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/proxy-overview.png">
			<media:title type="html">Proxy overview: three proxies connecting VS Code to different backends</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/testapp-health.png">
			<media:title type="html">Proxies test app — health dashboard showing all three proxies green</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/testapp-models.png">
			<media:title type="html">Foundry Local model management — load, unload, delete cached models</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/testapp-chat.png">
			<media:title type="html">Chat page — pick a proxy, model, and send a message with streaming support</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/testapp-compare.png">
			<media:title type="html">Compare page — same prompt sent to all three proxies simultaneously</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/aspire-dashboard.png">
			<media:title type="html">Aspire dashboard — all four services running and healthy</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/aspire-traces.png">
			<media:title type="html">Aspire traces — LLM spans with latency and token counts</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.CopilotHarness/raw/main/docs/blog/01-CopilotHarness-images/testapp-setup.png">
			<media:title type="html">Setup page — auto-generated VS Code config snippets for each proxy</media:title>
		</media:content>
	</item>
		<item>
		<title>Local-first AI Agents in C#: Foundry Local, MEAI, and Microsoft Agent Framework</title>
		<link>https://elbruno.com/2026/06/05/local-first-ai-agents-in-c-foundry-local-meai-and-microsoft-agent-framework/</link>
					<comments>https://elbruno.com/2026/06/05/local-first-ai-agents-in-c-foundry-local-meai-and-microsoft-agent-framework/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Fri, 05 Jun 2026 19:43:12 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Foundry Local]]></category>
		<category><![CDATA[MAF]]></category>
		<category><![CDATA[MEAI]]></category>
		<category><![CDATA[Microsoft Agent Framework]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40751</guid>

					<description><![CDATA[Hi! If you are building local AI apps in C#, you quickly hit a practical gap: Foundry Local SDK is great for local/on-device inference. Microsoft.Extensions.AI and Microsoft Agent Framework expect an IChatClient. There is no first-party Foundry Local -&#62; IChatClient bridge yet. That is exactly why I created this library Nuget: https://www.nuget.org/packages/ElBruno.MAF.FoundryLocal.Adapter Repo: https://github.com/elbruno/ElBruno.MAF.FoundryLocal/ Sample: ElBruno.MAF.FoundryLocal/src/ElBruno.MAF.FoundryLocal.Console at main · elbruno/ElBruno.MAF.FoundryLocal Why I [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">If you are building local AI apps in C#, you quickly hit a practical gap:</p>



<ul class="wp-block-list">
<li><strong>Foundry Local SDK</strong> is great for local/on-device inference.</li>



<li><strong>Microsoft.Extensions.AI</strong> and <strong>Microsoft Agent Framework</strong> expect an <code>IChatClient</code>.</li>



<li>There is no first-party Foundry Local -&gt; <code>IChatClient</code> bridge yet.</li>
</ul>



<p class="wp-block-paragraph">That is exactly why I created this library</p>



<ul class="wp-block-list">
<li>Nuget: <a href="https://www.nuget.org/packages/ElBruno.MAF.FoundryLocal.Adapter">https://www.nuget.org/packages/ElBruno.MAF.FoundryLocal.Adapter</a></li>



<li>Repo: <a href="https://github.com/elbruno/ElBruno.MAF.FoundryLocal/">https://github.com/elbruno/ElBruno.MAF.FoundryLocal/</a></li>



<li>Sample: <a href="https://github.com/elbruno/ElBruno.MAF.FoundryLocal/tree/main/src/ElBruno.MAF.FoundryLocal.Console">ElBruno.MAF.FoundryLocal/src/ElBruno.MAF.FoundryLocal.Console at main · elbruno/ElBruno.MAF.FoundryLocal</a></li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Why I created this<a href="https://github.com/elbruno/ElBruno.MAF.FoundryLocal/blob/main/docs/blog-post-using-foundry-local-wrapper.md#why-i-created-this"></a></h2>



<p class="wp-block-paragraph">I wanted a&nbsp;<strong>clean, non-REST, in-process</strong>&nbsp;integration where:</p>



<ol class="wp-block-list">
<li>Foundry Local stays local and private.</li>



<li>My app code uses standard MEAI abstractions (<code>IChatClient</code>).</li>



<li>The same <code>IChatClient</code> can be reused by Microsoft Agent Framework.</li>
</ol>



<p class="wp-block-paragraph">So the library provides a thin adapter:</p>



<p class="wp-block-paragraph"><code>Foundry Local SDK</code>&nbsp;-&gt;&nbsp;<code>FoundryLocalChatClientAdapter</code>&nbsp;-&gt;&nbsp;<code>IChatClient</code></p>



<p class="wp-block-paragraph">This lets you write provider-agnostic app code while still running local inference.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Minimal usage</h2>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.MAF.FoundryLocal/blob/main/docs/blog-post-using-foundry-local-wrapper.md#minimal-usage"></a></p>



<p class="wp-block-paragraph">Install:</p>



<pre class="wp-block-preformatted"><strong>dotnet add package ElBruno.MAF.FoundryLocal.Adapter<br />dotnet add package Microsoft.Extensions.Hosting<br />dotnet add package Microsoft.Agents.AI</strong></pre>



<p class="wp-block-paragraph">Use the wrapper as&nbsp;<code>IChatClient</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using ElBruno.MAF.FoundryLocal;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

builder.Services.Configure&lt;FoundryLocalOptions&gt;(o =&gt;
{
    o.ModelAlias = &quot;qwen2.5-0.5b&quot;;
    o.DownloadIfMissing = true;
    o.UnloadOnExit = true;
});

builder.Services.Configure&lt;ChatRuntimeOptions&gt;(_ =&gt; { });
builder.Services.AddSingleton&lt;FoundryLocalModelLifecycleService&gt;();
builder.Services.AddSingleton&lt;IChatClient, FoundryLocalChatClientAdapter&gt;();

using var host = builder.Build();
var chatClient = host.Services.GetRequiredService&lt;IChatClient&gt;();

var response = await chatClient.GetResponseAsync(
&#91;
    new(ChatRole.User, &quot;Explain local-first AI in one paragraph.&quot;)
]);

Console.WriteLine(response.Text);
</pre></div>


<pre class="wp-block-preformatted"></pre>



<p class="wp-block-paragraph">Reuse the same wrapper with Microsoft Agent Framework:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Microsoft.Agents.AI;

var agent = new ChatClientAgent(
    chatClient: chatClient,
    instructions: "You are a concise local assistant.",
    name: "LocalFoundryAgent",
    description: "Foundry Local + IChatClient adapter",
    tools: null,
    loggerFactory: null,
    services: null);

var agentResponse = await agent.RunAsync(
    "Give me 3 bullet points about local AI in .NET.",
    session: null,
    options: null);

Console.WriteLine(agentResponse);
</pre></div>


<pre class="wp-block-preformatted"></pre>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">What this gives you<a href="https://github.com/elbruno/ElBruno.MAF.FoundryLocal/blob/main/docs/blog-post-using-foundry-local-wrapper.md#what-this-gives-you"></a></h2>



<ul class="wp-block-list">
<li>Local inference through Foundry Local SDK</li>



<li>No local REST server required</li>



<li>Standard MEAI <code>IChatClient</code> programming model</li>



<li>Easy path to Agent Framework agents</li>
</ul>



<p class="wp-block-paragraph">In short:&nbsp;<strong>local model runtime + standard .NET AI abstractions + minimal glue code</strong>.</p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/06/05/local-first-ai-agents-in-c-foundry-local-meai-and-microsoft-agent-framework/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40751</post-id>
		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>
	</item>
		<item>
		<title>GitHub Copilot and tokens: how to keep using AI without burning your budget in three prompts (some personal lessons learned!)</title>
		<link>https://elbruno.com/2026/06/04/github-copilot-and-tokens-how-to-keep-using-ai-without-burning-your-budget-in-three-prompts-some-personal-lessons-learned/</link>
					<comments>https://elbruno.com/2026/06/04/github-copilot-and-tokens-how-to-keep-using-ai-without-burning-your-budget-in-three-prompts-some-personal-lessons-learned/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Thu, 04 Jun 2026 19:18:53 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[GitHub Copilot]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40745</guid>

					<description><![CDATA[Hi! For a long time, many of us used GitHub Copilot as if it were unlimited magic: autocomplete, chat, agent mode, code review, increasingly powerful models, massive context, and long-running sessions that sometimes felt like a pair-programming marathon. And it worked. Well, mostly. Now, with usage-based billing and AI Credits, many developers are seeing something [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-full"><img loading="lazy" width="836" height="471" src="https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-5-2026-08_55_47-amsmall.png" alt="" class="wp-image-40749" srcset="https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-5-2026-08_55_47-amsmall.png 836w, https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-5-2026-08_55_47-amsmall.png?w=150&amp;h=85 150w, https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-5-2026-08_55_47-amsmall.png?w=300&amp;h=169 300w, https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-5-2026-08_55_47-amsmall.png?w=768&amp;h=433 768w" sizes="auto, (max-width: 836px) 100vw, 836px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">For a long time, many of us used GitHub Copilot as if it were unlimited magic: autocomplete, chat, agent mode, code review, increasingly powerful models, massive context, and long-running sessions that sometimes felt like a pair-programming marathon.</p>



<p class="wp-block-paragraph">And it worked. Well, mostly.</p>



<p class="wp-block-paragraph">Now, with usage-based billing and AI Credits, many developers are seeing something that used to be mostly invisible: every AI interaction has a cost. And that cost is not only about “asking a question.” It depends on the model, the context, input tokens, output tokens, cached tokens, tools, files, logs, MCP servers, and how long we let an agent keep working.</p>



<p class="wp-block-paragraph">GitHub explains this in the Copilot billing documentation: interactions consume input, output, and cached tokens; each model has its own pricing; and the total is converted into AI Credits. The same documentation also explains an important detail: code completions and Next Edit Suggestions are not charged as AI Credits in paid plans.</p>



<p class="wp-block-paragraph">Source:&nbsp;<a href="https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing">https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing</a></p>



<p class="wp-block-paragraph">The natural reaction is to panic.</p>



<p class="wp-block-paragraph">The useful reaction is to optimize.</p>



<p class="wp-block-paragraph">Just like we optimize compute, storage, bandwidth, or GitHub Actions minutes, now we also need to optimize how we use tokens.</p>



<p class="wp-block-paragraph">And yes, this applies to those of us using Copilot every day for .NET, AI, Azure, scripts, demos, documentation, refactors, and those beautiful moments when we tell the agent “just fix this test” and come back 20 minutes later to find a doctoral thesis in progress.</p>



<h2 class="wp-block-heading">The real problem is not the prompt. It is the context</h2>



<p class="wp-block-paragraph">When we talk about tokens, we often think only about the text we type.</p>



<p class="wp-block-paragraph">But in AI-assisted development tools, the expensive part is often everything that travels around the prompt:</p>



<ul class="wp-block-list">
<li>chat history</li>



<li>open files</li>



<li>files attached as context</li>



<li>workspace search results</li>



<li>diffs</li>



<li>terminal output</li>



<li>build errors</li>



<li>long logs</li>



<li>tool calls</li>



<li>MCP server responses</li>



<li>custom instructions</li>



<li>agent memory</li>



<li>content the model decides to inspect while completing the task</li>
</ul>



<p class="wp-block-paragraph">A one-line question can be cheap.</p>



<p class="wp-block-paragraph">A one-line question inside a conversation with 80 messages, 12 files, 3 logs, 5 tools, and an MCP server connected to half the universe… not so much.</p>



<p class="wp-block-paragraph">The first optimization is mental:&nbsp;<strong>more context does not always mean a better answer</strong>.</p>



<p class="wp-block-paragraph">Sometimes more context only means more tokens, more noise, and more chances for the model to get distracted.</p>



<h2 class="wp-block-heading">1. Use autocomplete and Next Edit Suggestions before opening chat</h2>



<p class="wp-block-paragraph">Not everything needs a conversation.</p>



<p class="wp-block-paragraph">For small tasks, Copilot directly in the editor is often the most efficient option:</p>



<ul class="wp-block-list">
<li>completing a line</li>



<li>finishing a simple function</li>



<li>generating boilerplate</li>



<li>suggesting the next obvious change</li>



<li>completing a repeated pattern</li>



<li>adjusting names</li>



<li>writing a simple condition</li>



<li>generating a property, DTO, or mapping</li>
</ul>



<p class="wp-block-paragraph">If you can solve it with&nbsp;<code>Tab</code>, do not open a chat.</p>



<p class="wp-block-paragraph">This is not just convenience. It is strategy. According to GitHub documentation, code completions and Next Edit Suggestions are not billed as AI Credits in paid plans.</p>



<p class="wp-block-paragraph">Source:&nbsp;<a href="https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing">https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing</a></p>



<p class="wp-block-paragraph">Simple rule:</p>



<ul class="wp-block-list">
<li>Use autocomplete for micro-tasks.</li>



<li>Use inline edit for local changes.</li>



<li>Use chat for questions that require reasoning.</li>



<li>Use agent mode for well-scoped multi-file tasks.</li>



<li>Use cloud agents when you really want to delegate a workflow, not when you only need to change three lines.</li>
</ul>



<p class="wp-block-paragraph">The most expensive model in the world should not be helping you write&nbsp;<code>public string Name { get; set; }</code>.</p>



<p class="wp-block-paragraph">That is what&nbsp;<code>Tab</code>&nbsp;is for. And coffee.</p>



<h2 class="wp-block-heading">2. Choose the right model for the task</h2>



<p class="wp-block-paragraph">Not every model has the same cost or the same purpose.</p>



<p class="wp-block-paragraph">The VS Code documentation recommends using lighter models for quick edits, boilerplate, and direct questions, and reserving reasoning models for complex refactors, architecture decisions, and multi-step debugging.</p>



<p class="wp-block-paragraph">Source:&nbsp;<a href="https://code.visualstudio.com/docs/copilot/guides/optimize-usage">https://code.visualstudio.com/docs/copilot/guides/optimize-usage</a></p>



<p class="wp-block-paragraph">A practical pattern:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Task type</th><th class="has-text-align-left" data-align="left">Recommended model</th></tr></thead><tbody><tr><td class="has-text-align-left" data-align="left">Simple question</td><td class="has-text-align-left" data-align="left">Lightweight model or Auto</td></tr><tr><td class="has-text-align-left" data-align="left">Boilerplate</td><td class="has-text-align-left" data-align="left">Lightweight model</td></tr><tr><td class="has-text-align-left" data-align="left">Code explanation</td><td class="has-text-align-left" data-align="left">Lightweight or medium model</td></tr><tr><td class="has-text-align-left" data-align="left">Simple tests</td><td class="has-text-align-left" data-align="left">Lightweight or medium model</td></tr><tr><td class="has-text-align-left" data-align="left">Complex debugging</td><td class="has-text-align-left" data-align="left">Reasoning model</td></tr><tr><td class="has-text-align-left" data-align="left">Architecture</td><td class="has-text-align-left" data-align="left">Reasoning or frontier model</td></tr><tr><td class="has-text-align-left" data-align="left">Large refactor</td><td class="has-text-align-left" data-align="left">Powerful model, but with limited scope</td></tr><tr><td class="has-text-align-left" data-align="left">Initial documentation</td><td class="has-text-align-left" data-align="left">Lightweight or local model</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">GitHub also documents Auto Model Selection, which can choose a model based on task complexity, availability, and policies. The documentation also notes that Auto can improve efficiency by reserving more expensive models for tasks that actually need them.</p>



<p class="wp-block-paragraph">Source:&nbsp;<a href="https://docs.github.com/en/copilot/concepts/auto-model-selection">https://docs.github.com/en/copilot/concepts/auto-model-selection</a></p>



<p class="wp-block-paragraph">My recommendation for most developers:</p>



<ul class="wp-block-list">
<li>use&nbsp;<strong>Auto</strong>&nbsp;as the default</li>



<li>manually switch to a more powerful model only when you have a clear reason</li>



<li>switch back to Auto or a cheaper model when the complex task is done</li>
</ul>



<p class="wp-block-paragraph">Do not drive a truck to buy bread.</p>



<p class="wp-block-paragraph">And do not use the most expensive model to ask how to center a&nbsp;<code>div</code>. Although, to be fair, sometimes centering a&nbsp;<code>div</code>&nbsp;does deserve an architecture review.</p>



<h2 class="wp-block-heading">3. Start new chats when you change tasks</h2>



<p class="wp-block-paragraph">This is one of the simplest and most ignored optimizations.</p>



<p class="wp-block-paragraph">The VS Code documentation is clear: when a conversation grows, it accumulates context from previous messages, tool outputs, and file contents. If you switch to an unrelated task inside the same session, the model still processes irrelevant history.</p>



<p class="wp-block-paragraph">Source:&nbsp;<a href="https://code.visualstudio.com/docs/copilot/guides/optimize-usage">https://code.visualstudio.com/docs/copilot/guides/optimize-usage</a></p>



<p class="wp-block-paragraph">Bad pattern:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Chat 1:</div><div class="cm-line">- Debug tests</div><div class="cm-line">- Then architecture</div><div class="cm-line">- Then generate README</div><div class="cm-line">- Then review Dockerfile</div><div class="cm-line">- Then explain an Azure error</div><div class="cm-line">- Then ask for a tweet</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Better pattern:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Chat 1: Debug tests</div><div class="cm-line">Chat 2: Architecture design</div><div class="cm-line">Chat 3: README</div><div class="cm-line">Chat 4: Dockerfile</div><div class="cm-line">Chat 5: Azure deployment issue</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">New task, new chat.</p>



<p class="wp-block-paragraph">Yes, it sounds simple.</p>



<p class="wp-block-paragraph">Yes, it works.</p>



<p class="wp-block-paragraph">And yes, it also helps your human brain, which sometimes has a smaller context window than the model.</p>



<h2 class="wp-block-heading">4. Use&nbsp;<code>/compact</code>&nbsp;and&nbsp;<code>/fork</code>&nbsp;when it makes sense</h2>



<p class="wp-block-paragraph">When a conversation has useful context but starts getting too large, you do not always need to throw it away.</p>



<p class="wp-block-paragraph">You can compact it or fork it.</p>



<p class="wp-block-paragraph">VS Code documents new sessions, forking, and compaction as ways to manage context and reduce unnecessary tokens.</p>



<p class="wp-block-paragraph">Source:&nbsp;<a href="https://code.visualstudio.com/docs/copilot/guides/optimize-usage">https://code.visualstudio.com/docs/copilot/guides/optimize-usage</a></p>



<p class="wp-block-paragraph">Good practices:</p>



<ul class="wp-block-list">
<li>use&nbsp;<code>/compact</code>&nbsp;when the conversation has useful information but too much history</li>



<li>use&nbsp;<code>/fork</code>&nbsp;when you want to explore an alternative without polluting the main conversation</li>



<li>start a new chat if the new task is unrelated</li>



<li>summarize the current state before continuing a long task</li>
</ul>



<p class="wp-block-paragraph">Useful prompt:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Summarize the current state, decisions made, files changed, and next steps. Keep it short and actionable.</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then copy that summary into a new conversation and continue with clean context.</p>



<p class="wp-block-paragraph">Less noise. Fewer tokens. Better focus.</p>



<h2 class="wp-block-heading">5. Do not ask it to analyze the whole repo if you only need three files</h2>



<p class="wp-block-paragraph">This is the classic one.</p>



<p class="wp-block-paragraph">Expensive prompt:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Analyze this entire repository and tell me what is wrong.</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Better prompt:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Analyze only these files:</div><div class="cm-line">- src/MyApp.Api/Program.cs</div><div class="cm-line">- src/MyApp.Core/Services/OrderService.cs</div><div class="cm-line">- tests/MyApp.Tests/OrderServiceTests.cs</div><div class="cm-line"></div><div class="cm-line">Goal: find why this test is failing.</div><div class="cm-line">Do not edit files yet. First explain the likely cause.</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The difference is huge.</p>



<p class="wp-block-paragraph">The first prompt invites the model to explore, read, search, open files, infer architecture, and consume context.</p>



<p class="wp-block-paragraph">The second prompt defines:</p>



<ul class="wp-block-list">
<li>files</li>



<li>goal</li>



<li>limits</li>



<li>working mode</li>



<li>expected output</li>
</ul>



<p class="wp-block-paragraph">In AI coding, scope is part of the prompt.</p>



<p class="wp-block-paragraph">Small scope, better result.</p>



<p class="wp-block-paragraph">Infinite scope, surprise in the bill.</p>



<h2 class="wp-block-heading">6. Separate planning, implementation, and validation</h2>



<p class="wp-block-paragraph">One of the most common mistakes with agent mode is asking for everything at once:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Analyze the issue, design the fix, implement it, run tests, fix errors, update docs, and create a summary.</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That sounds productive.</p>



<p class="wp-block-paragraph">But it can also trigger loops, tool calls, unnecessary changes, and high token consumption.</p>



<p class="wp-block-paragraph">A better approach is to use phases.</p>



<h3 class="wp-block-heading">Phase 1: plan</h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Create a short implementation plan. Do not modify files yet.</div><div class="cm-line">Focus only on the failing test and the minimal code path required to fix it.</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">Phase 2: scoped implementation</h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Implement step 1 only. Modify only the files listed in the plan.</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">Phase 3: validation</h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Run the relevant tests only. If they fail, explain the failure before changing code again.</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">Phase 4: cleanup</h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Now clean up the implementation without changing behavior. Keep the diff small.</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The VS Code documentation also recommends planning before implementation to reduce rework and back-and-forth.</p>



<p class="wp-block-paragraph">Source:&nbsp;<a href="https://code.visualstudio.com/docs/copilot/guides/optimize-usage">https://code.visualstudio.com/docs/copilot/guides/optimize-usage</a></p>



<p class="wp-block-paragraph">Sometimes the best prompt is not “do everything.”</p>



<p class="wp-block-paragraph">It is “think first, touch little, validate quickly.”</p>



<h2 class="wp-block-heading">7. Be careful with logs: do not paste a novel if you only need the error</h2>



<p class="wp-block-paragraph">Logs are one of the silent token killers.</p>



<p class="wp-block-paragraph">Typical example:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Here is my build log</div></code></pre>
		</div>
	</div>
</div>

<p>[paste 2,000 lines]</p>



<p class="wp-block-paragraph">And the real error was in the last 20 lines.</p>



<p class="wp-block-paragraph">Better:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Here are the last 40 lines of the failing build log. Focus on the first real error, not the cascading errors.</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Or even:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">This is the error:</div><div class="cm-line">CS0246: The type or namespace name &apos;X&apos; could not be found.</div><div class="cm-line">Relevant files:</div><div class="cm-line">- Program.cs</div><div class="cm-line">- MyService.cs</div><div class="cm-line"></div><div class="cm-line">What is the likely fix?</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Good practices:</p>



<ul class="wp-block-list">
<li>paste only the first relevant error</li>



<li>avoid full logs if errors are repeated</li>



<li>remove timestamps if they do not add value</li>



<li>remove duplicated stack traces</li>



<li>summarize what you already tried</li>



<li>include exact commands, not the whole terminal history</li>
</ul>



<p class="wp-block-paragraph">Copilot can help a lot with logs.</p>



<p class="wp-block-paragraph">But it does not need to read your CI/CD diary.</p>



<h2 class="wp-block-heading">8. Review your custom instructions</h2>



<p class="wp-block-paragraph">Custom instructions are fantastic.</p>



<p class="wp-block-paragraph">They can also become a token backpack if nobody maintains them.</p>



<p class="wp-block-paragraph">A good&nbsp;<code>.github/copilot-instructions.md</code>&nbsp;file is:</p>



<ul class="wp-block-list">
<li>short</li>



<li>specific</li>



<li>current</li>



<li>based on real repository rules</li>



<li>clear about build/test commands</li>



<li>clear about important conventions</li>
</ul>



<p class="wp-block-paragraph">A bad one is:</p>



<ul class="wp-block-list">
<li>too long</li>



<li>duplicated</li>



<li>contradictory</li>



<li>based on old architecture</li>



<li>full of rules nobody follows</li>



<li>full of generic instructions that apply to every repo on the planet</li>
</ul>



<p class="wp-block-paragraph">Example of useful instructions:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line"># Copilot instructions</div><div class="cm-line"></div><div class="cm-line">- Use C# 13 and .NET 10 conventions.</div><div class="cm-line">- Keep changes minimal and focused.</div><div class="cm-line">- Do not introduce new dependencies without explaining why.</div><div class="cm-line">- Run `dotnet build -c Release` after code changes.</div><div class="cm-line">- Run relevant tests only unless asked for the full suite.</div><div class="cm-line">- Prefer Aspire service defaults when adding services.</div><div class="cm-line">- Do not modify generated files.</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">You do not need to write a national constitution for Copilot to understand your repo.</p>



<p class="wp-block-paragraph">You need short rules that reduce repeated decisions.</p>



<h2 class="wp-block-heading">9. Review the MCP servers and tools you have enabled</h2>



<p class="wp-block-paragraph">This is one of the areas where many developers may be consuming context without realizing it.</p>



<p class="wp-block-paragraph">MCP is powerful because it lets agents connect to tools, resources, prompts, and external systems. But every server and every available tool can also affect context, tool selection, and the way the agent works.</p>



<p class="wp-block-paragraph">The VS Code documentation explains that MCP servers can expose tools, resources, prompts, and apps. It also allows developers to enable, disable, install, configure, and manage MCP servers from VS Code.</p>



<p class="wp-block-paragraph">Source:&nbsp;<a href="https://code.visualstudio.com/docs/copilot/customization/mcp-servers">https://code.visualstudio.com/docs/copilot/customization/mcp-servers</a></p>



<p class="wp-block-paragraph">Practical recommendations:</p>



<ul class="wp-block-list">
<li>do not keep every MCP server enabled all the time</li>



<li>enable only what you need for the current workspace</li>



<li>disable experimental MCP servers when you are not using them</li>



<li>review duplicated or overly generic tools</li>



<li>review tool descriptions: if they are too long or confusing, they may hurt tool selection</li>



<li>avoid MCP servers that return huge responses by default</li>



<li>limit resources that add too much context</li>



<li>check whether a server is bringing more information than needed</li>



<li>review MCP logs when something behaves strangely</li>
</ul>



<p class="wp-block-paragraph">Example:</p>



<p class="wp-block-paragraph">If you are working on a local .NET API, maybe you do not need all of these enabled at the same time:</p>



<ul class="wp-block-list">
<li>browser automation</li>



<li>extended filesystem</li>



<li>cloud docs search</li>



<li>GitHub</li>



<li>Jira</li>



<li>Slack</li>



<li>database explorer</li>



<li>Kubernetes</li>



<li>Playwright</li>



<li>internal wiki</li>
</ul>



<p class="wp-block-paragraph">Every extra tool can be useful.</p>



<p class="wp-block-paragraph">But it can also expand the agent’s decision space.</p>



<p class="wp-block-paragraph">And when an agent has too many tools, sometimes the problem is not lack of capability. It is too many temptations.</p>



<p class="wp-block-paragraph">My personal rule:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">MCP servers should be workspace-specific, not personality traits.</p>
</blockquote>



<p class="wp-block-paragraph">Enable what you need. Turn off what you do not.</p>



<h2 class="wp-block-heading">10. Use traditional tools for traditional work</h2>



<p class="wp-block-paragraph">Not everything needs AI.</p>



<p class="wp-block-paragraph">For many tasks, traditional tools are better, faster, and cheaper:</p>



<ul class="wp-block-list">
<li>formatter for formatting</li>



<li>linter for style</li>



<li>compiler for type errors</li>



<li>tests for validation</li>



<li>static analyzers for known rules</li>



<li>dependency scanners for known vulnerabilities</li>



<li>scripts for repeatable tasks</li>
</ul>



<p class="wp-block-paragraph">Copilot is excellent for reasoning, explaining, proposing, connecting ideas, and accelerating implementation.</p>



<p class="wp-block-paragraph">But if you use a frontier model to discover a missing&nbsp;<code>using</code>, something went sideways.</p>



<p class="wp-block-paragraph">Good pattern:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Run the build. Give Copilot only the first relevant compiler error. Ask for the minimal fix.</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Bad pattern:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Ask Copilot to inspect the entire repository and find why the build might fail.</div><div class="cm-line"></div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">First, let deterministic tools do their job.</p>



<p class="wp-block-paragraph">Then use AI where it adds value.</p>



<h2 class="wp-block-heading">11. Local models: they do not replace Copilot, but they can complement it very well</h2>



<p class="wp-block-paragraph">We are going to see more PCs with interesting local AI capabilities: stronger GPUs, NPUs, compact workstations, and machines designed to run models locally. NVIDIA, for example, positions RTX Spark as compact PCs and laptops with NVIDIA AI and RTX graphics capabilities.</p>



<p class="wp-block-paragraph">Source:&nbsp;<a href="https://www.nvidia.com/en-us/products/rtx-spark/">https://www.nvidia.com/en-us/products/rtx-spark/</a></p>



<p class="wp-block-paragraph">This raises an interesting question:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Does everything need to go to the cloud?</p>
</blockquote>



<p class="wp-block-paragraph">Not necessarily.</p>



<p class="wp-block-paragraph">There are tasks where a local model may be enough:</p>



<ul class="wp-block-list">
<li>summarizing logs</li>



<li>generating documentation drafts</li>



<li>explaining small code snippets</li>



<li>creating scaffolding</li>



<li>generating initial tests</li>



<li>transforming text</li>



<li>preparing prompts</li>



<li>analyzing snippets</li>



<li>creating session summaries</li>



<li>reviewing basic style</li>
</ul>



<p class="wp-block-paragraph">And there are tasks where cloud/frontier models still make a lot of sense:</p>



<ul class="wp-block-list">
<li>complex multi-file debugging</li>



<li>deep reasoning</li>



<li>large migrations</li>



<li>high-risk refactors</li>



<li>architecture</li>



<li>long agentic workflows</li>



<li>direct integration with GitHub, PRs, issues, and CI</li>
</ul>



<p class="wp-block-paragraph">The idea is not “local vs cloud.”</p>



<p class="wp-block-paragraph">The idea is&nbsp;<strong>local for simple work, cloud for work that really needs cloud</strong>.</p>



<p class="wp-block-paragraph">This post is not about advanced BYOK, routing, or gateway architectures. That deserves its own post.</p>



<p class="wp-block-paragraph">But as a baseline idea: if you can solve repetitive tasks with local models, you can reserve Copilot and powerful models for the tasks where they really shine.</p>



<h2 class="wp-block-heading">12. Code review: use it where it adds the most value</h2>



<p class="wp-block-paragraph">Copilot code review can be very useful, but it also has a cost.</p>



<p class="wp-block-paragraph">GitHub documentation explains that Copilot code review is billed in two ways: token consumption through AI Credits and GitHub Actions minutes for the agentic review infrastructure.</p>



<p class="wp-block-paragraph">Source:&nbsp;<a href="https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing">https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing</a></p>



<p class="wp-block-paragraph">Recommendations:</p>



<ul class="wp-block-list">
<li>do not enable high-effort automatic review for every PR without measuring it</li>



<li>use linters and analyzers for mechanical rules</li>



<li>reserve Copilot review for meaningful PRs</li>



<li>define when to use standard vs higher effort</li>



<li>avoid full AI review if you only changed documentation</li>



<li>review usage by repository/team if you are in an organization</li>
</ul>



<p class="wp-block-paragraph">Copilot review can be excellent.</p>



<p class="wp-block-paragraph">But you do not need an AI reviewer philosophically inspecting a three-line README change.</p>



<h2 class="wp-block-heading">13. Define usage profiles for your team</h2>



<p class="wp-block-paragraph">In enterprise teams, the worst-case scenario is letting everyone use any model, in any mode, for any task, with no guidance.</p>



<p class="wp-block-paragraph">You do not need to block everything.</p>



<p class="wp-block-paragraph">You need to educate people and provide clear profiles.</p>



<h3 class="wp-block-heading">Daily coding profile</h3>



<ul class="wp-block-list">
<li>Auto Model Selection</li>



<li>autocomplete and Next Edit Suggestions first</li>



<li>short chat sessions</li>



<li>limited context</li>



<li>lightweight models for simple questions</li>
</ul>



<h3 class="wp-block-heading">Debugging profile</h3>



<ul class="wp-block-list">
<li>minimal logs</li>



<li>first relevant error</li>



<li>specific files</li>



<li>plan before changes</li>



<li>relevant tests, not always the full suite</li>
</ul>



<h3 class="wp-block-heading">Refactor profile</h3>



<ul class="wp-block-list">
<li>plan first</li>



<li>scope by folder or feature</li>



<li>changes in phases</li>



<li>more powerful model only during the hard part</li>



<li>frequent validation</li>
</ul>



<h3 class="wp-block-heading">Documentation profile</h3>



<ul class="wp-block-list">
<li>lightweight or local model</li>



<li>specific files</li>



<li>limited output</li>



<li>no agent mode unless needed</li>
</ul>



<h3 class="wp-block-heading">Agent mode profile</h3>



<ul class="wp-block-list">
<li>clear issue</li>



<li>clear stop condition</li>



<li>defined scope</li>



<li>defined validation commands</li>



<li>do not let it run unsupervised if the goal is ambiguous</li>
</ul>



<p class="wp-block-paragraph">This is not about saying “do not use Copilot.”</p>



<p class="wp-block-paragraph">It is about saying “use the right mode for the right job.”</p>



<h2 class="wp-block-heading">14. Quick checklist for developers</h2>



<p class="wp-block-paragraph">Before sending the next prompt, ask yourself:</p>



<ul class="wp-block-list">
<li>Can I solve this with autocomplete?</li>



<li>Do I need chat, or is inline edit enough?</li>



<li>Do I need agent mode, or just an explanation?</li>



<li>Am I using Auto, or am I using a model that is too expensive for the task?</li>



<li>Does this conversation already have too much history?</li>



<li>Should I start a new chat?</li>



<li>Can I pass only two or three files?</li>



<li>Can I paste only the relevant error?</li>



<li>Can I ask for a plan first?</li>



<li>Do I have MCP servers enabled that I do not need?</li>



<li>Are my custom instructions too long?</li>



<li>Can a linter/test/build answer this before AI?</li>



<li>Could this task go to a local model?</li>
</ul>



<p class="wp-block-paragraph">If the answer is “yes” to several of these, you can probably save tokens without losing productivity.</p>



<h2 class="wp-block-heading">15. Checklist for teams</h2>



<p class="wp-block-paragraph">For organizations, I would start here:</p>



<ul class="wp-block-list">
<li>review usage by user, team, and repository</li>



<li>understand which models are used the most</li>



<li>review how much usage comes from agent mode</li>



<li>review Copilot code review usage</li>



<li>define internal model selection guidelines</li>



<li>promote Auto as the default</li>



<li>teach small, scoped prompts</li>



<li>clean up custom instructions by repository</li>



<li>review recommended and allowed MCP servers</li>



<li>create usage profiles by task type</li>



<li>measure before and after changes</li>



<li>do not block AI out of fear</li>



<li>govern it like any other cloud resource</li>
</ul>



<p class="wp-block-paragraph">Because this looks a lot like cloud cost optimization.</p>



<p class="wp-block-paragraph">First, everyone celebrated how easy it was to create resources.</p>



<p class="wp-block-paragraph">Then the bill arrived.</p>



<p class="wp-block-paragraph">Then we learned FinOps.</p>



<p class="wp-block-paragraph">Now we need something similar for AI-assisted development.</p>



<h2 class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">The new GitHub Copilot usage model does not mean we need to stop using AI to code.</p>



<p class="wp-block-paragraph">It means we can no longer treat every interaction as free, infinite, and invisible.</p>



<p class="wp-block-paragraph">The good news is that many optimizations are simple:</p>



<ul class="wp-block-list">
<li>use autocomplete before chat</li>



<li>choose the model intentionally</li>



<li>start new chats</li>



<li>reduce context</li>



<li>limit logs</li>



<li>separate planning and implementation</li>



<li>review MCP servers</li>



<li>clean up custom instructions</li>



<li>use traditional tools when they fit</li>



<li>reserve powerful models for powerful problems</li>



<li>consider local models for simple tasks</li>
</ul>



<p class="wp-block-paragraph">The goal is not to use less Copilot.</p>



<p class="wp-block-paragraph">The goal is to use fewer tokens and get better results.</p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/06/04/github-copilot-and-tokens-how-to-keep-using-ai-without-burning-your-budget-in-three-prompts-some-personal-lessons-learned/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40745</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-5-2026-08_55_47-amsmall.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-5-2026-08_55_47-amsmall.png">
			<media:title type="html">ChatGPT Image Jun 5, 2026, 08_55_47 AM=small</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>
	</item>
		<item>
		<title>MAI-Image-2.5 support in ElBruno.Text2Image and the t2i CLI &#128640;</title>
		<link>https://elbruno.com/2026/06/03/mai-image-2-5-support-in-elbruno-text2image-and-the-t2i-cli-%f0%9f%9a%80/</link>
					<comments>https://elbruno.com/2026/06/03/mai-image-2-5-support-in-elbruno-text2image-and-the-t2i-cli-%f0%9f%9a%80/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Wed, 03 Jun 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Foundry]]></category>
		<category><![CDATA[MAI-Image]]></category>
		<category><![CDATA[NuGet]]></category>
		<category><![CDATA[Text-To-Image]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40739</guid>

					<description><![CDATA[Hi! And for the Flash version, use the same generator with the Flash model id:]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="576" src="https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-2-2026-09_00_33-pm.png?w=1024" alt="" class="wp-image-40743" srcset="https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-2-2026-09_00_33-pm.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-2-2026-09_00_33-pm.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-2-2026-09_00_33-pm.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-2-2026-09_00_33-pm.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-2-2026-09_00_33-pm.png?w=1440 1440w, https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-2-2026-09_00_33-pm.png 1672w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<div class="wp-block-jetpack-markdown"><p>Microsoft Build brought a wave of new AI model announcements, and one of the fun ones for image generation is <strong>MAI-Image-2.5</strong>.</p>
<p>So, of course, I had to add support for it in my .NET image generation library and CLI tool <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>The latest version of <strong>ElBruno.Text2Image</strong> now supports:</p>
<ul>
<li><strong>MAI-Image-2.5</strong></li>
<li><strong>MAI-Image-2.5-Flash</strong></li>
</ul>
<p>This means you can generate images using the new MAI models from a .NET app or directly from the terminal with the <code>t2i</code> CLI.</p>
<h2>Links</h2>
<p>Official model announcement:</p>
<p><a href="https://microsoft.ai/models/mai-image-2-5/" rel="nofollow">https://microsoft.ai/models/mai-image-2-5/</a></p>
<p>NuGet packages:</p>
<ul>
<li>ElBruno.Text2Image: <a href="https://www.nuget.org/packages/ElBruno.Text2Image" rel="nofollow">https://www.nuget.org/packages/ElBruno.Text2Image</a></li>
<li>ElBruno.Text2Image.Cli: <a href="https://www.nuget.org/packages/ElBruno.Text2Image.Cli" rel="nofollow">https://www.nuget.org/packages/ElBruno.Text2Image.Cli</a></li>
</ul>
<p>Source code:</p>
<p><a href="https://github.com/elbruno/ElBruno.Text2Image" rel="nofollow">https://github.com/elbruno/ElBruno.Text2Image</a></p>
<h2>Install the CLI</h2>
<pre><code class="language-bash">dotnet tool install --global ElBruno.Text2Image.Cli
</code></pre>
<p>Or update it if you already have it installed:</p>
<pre><code class="language-bash">dotnet tool update --global ElBruno.Text2Image.Cli
</code></pre>
<h2>Generate an image from the terminal</h2>
<p>Using MAI-Image-2.5:</p>
<pre><code class="language-bash">t2i --provider foundry-mai25 &quot;a photograph of a red fox in an autumn forest&quot;
</code></pre>
<p>Using MAI-Image-2.5-Flash:</p>
<pre><code class="language-bash">t2i --provider foundry-mai25-flash &quot;a quick concept sketch of a city skyline&quot;
</code></pre>
<p>That’s the part I like the most:</p>
<pre><code class="language-text">prompt -&gt; terminal -&gt; image
</code></pre>
<p>Simple and fun.</p>
<h2>Use it from C#</h2>
<p>You can also use the new MAI-Image-2.5 model from a .NET app.</p>
</div>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using ElBruno.Text2Image;
using ElBruno.Text2Image.Foundry;

using var generator = new MaiImage25Generator(
    endpoint: "https://your-resource.services.ai.azure.com",
    apiKey: "your-api-key",
    httpClient: new HttpClient(),
    modelName: "MAI-Image-2.5",
    modelId: "MAI-Image-2.5");

var result = await generator.GenerateAsync(
    "A photograph of a red fox in an autumn forest");

await result.SaveAsync("mai-image25-output.png");
</pre></div>


<p class="wp-block-paragraph">And for the Flash version, use the same generator with the Flash model id:<br /></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using ElBruno.Text2Image;
using ElBruno.Text2Image.Foundry;

using var generator = new MaiImage25Generator(
    endpoint: "https://your-resource.services.ai.azure.com",
    apiKey: "your-api-key",
    httpClient: new HttpClient(),
    modelName: "MAI-Image-2.5-Flash",
    modelId: "MAI-Image-2.5-Flash");

var result = await generator.GenerateAsync(
    "A clean comic-style illustration of a happy developer building AI apps");

await result.SaveAsync("mai-image25-flash-output.png");
</pre></div>


<div class="wp-block-jetpack-markdown"><h2>Why this is useful</h2>
<p>The goal of this project is simple:</p>
<ul>
<li>Use image generation from .NET</li>
<li>Support cloud and local models</li>
<li>Keep the API clean</li>
<li>Make the CLI useful for developers, demos, agents, and automation</li>
<li>Avoid needing a Python environment for every image generation experiment</li>
</ul>
<p>And now with MAI-Image-2.5 and MAI-Image-2.5-Flash, we have more options to experiment with image generation from .NET and from the command line.</p>
<p>Build announcements are fun.
Build announcements that I can use from a terminal?
Even better <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f60e.png" alt="😎" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
</div>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/06/03/mai-image-2-5-support-in-elbruno-text2image-and-the-t2i-cli-%f0%9f%9a%80/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40739</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-2-2026-09_00_33-pm.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-2-2026-09_00_33-pm.png">
			<media:title type="html">ChatGPT Image Jun 2, 2026, 09_00_33 PM</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/06/chatgpt-image-jun-2-2026-09_00_33-pm.png?w=1024"/>
	</item>
		<item>
		<title>VS Code 1.122 Makes BYOK Easier</title>
		<link>https://elbruno.com/2026/05/30/vs-code-1-122-makes-byok-easier/</link>
					<comments>https://elbruno.com/2026/05/30/vs-code-1-122-makes-byok-easier/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Sun, 31 May 2026 00:52:45 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[BYOK]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[GitHub Copilot]]></category>
		<category><![CDATA[Visual Studio Code]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40732</guid>

					<description><![CDATA[One of the most interesting announcements in the recent VS Code 1.122 release is that Bring Your Own Key (BYOK) models can now be used without signing in to GitHub. VS Code now supports BYOK scenarios, including chat, tools, and MCP integrations, without requiring GitHub authentication, making enterprise, offline, and air-gapped workflows much easier to [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="576" src="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-30-2026-08_49_35-pm.png?w=1024" alt="" class="wp-image-40736" srcset="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-30-2026-08_49_35-pm.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-30-2026-08_49_35-pm.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-30-2026-08_49_35-pm.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-30-2026-08_49_35-pm.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-30-2026-08_49_35-pm.png?w=1440 1440w, https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-30-2026-08_49_35-pm.png 1672w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">One of the most interesting announcements in the recent VS Code 1.122 release is that Bring Your Own Key (BYOK) models can now be used <strong>without signing in to GitHub</strong>. VS Code now supports BYOK scenarios, including chat, tools, and MCP integrations, without requiring GitHub authentication, making enterprise, offline, and air-gapped workflows much easier to support.</p>



<p class="wp-block-paragraph">I originally came across this update thanks to a tweet from Pierce Boggan:</p>



<figure class="wp-block-embed is-type-rich is-provider-x wp-block-embed-x"><div class="wp-block-embed__wrapper">
<div class="embed-x"><blockquote class="twitter-tweet" data-width="500" data-dnt="true"><p lang="en" dir="ltr">VS Code gives you: <br><br>1. SOTA models from Anthropic, OpenAI, Gemini<br>2. BYOK for any model not natively supported in the product (and works w/o login!) <br>3. Harness choice with VS Code, Copilot CLI, Copilot cloud agent, and 3p harnesses like Claude/Codex <br><br>Developer choice ftw! <a href="https://t.co/mm7n1owQK5">https://t.co/mm7n1owQK5</a></p>&mdash; Pierce Boggan (@pierceboggan) <a href="https://x.com/pierceboggan/status/2060837045195649345?ref_src=twsrc%5Etfw">May 30, 2026</a></blockquote><script async src="https://platform.x.com/widgets.js" charset="utf-8"></script></div>
</div></figure>



<p class="wp-block-paragraph">This immediately caught my attention because it connects directly with the experiments I’ve been running around GitHub Copilot CLI, local models, BYOK, SQUAD, and AI-assisted software engineering.</p>



<p class="wp-block-paragraph">Quick recap:</p>



<p class="wp-block-paragraph">CPU-only local models<br />Slow, useful mostly for questions and tiny tasks.<br /><a href="https://elbruno.com/2026/05/03/running-github-copilot-cli-offline-with-local-models-a-cpu-only-reality-check/">https://elbruno.com/2026/05/03/running-github-copilot-cli-offline-with-local-models-a-cpu-only-reality-check/</a></p>



<p class="wp-block-paragraph">GPU local models<br />Faster, with more room for local agent experiments.<br /><a href="https://elbruno.com/2026/05/06/running-github-copilot-cli-offline-with-local-models-gpu-edition/">https://elbruno.com/2026/05/06/running-github-copilot-cli-offline-with-local-models-gpu-edition/</a></p>



<p class="wp-block-paragraph">GPT-5-mini BYOK<br />Capable, but stabilization, quality gates, and validation became the real work.<br /><a href="https://elbruno.com/2026/05/11/github-copilot-cli-gpt-5-mini-byok-the-code-was-cheap-the-quality-gates-were-expensive/">https://elbruno.com/2026/05/11/github-copilot-cli-gpt-5-mini-byok-the-code-was-cheap-the-quality-gates-were-expensive/</a></p>



<p class="wp-block-paragraph">GPT-5.5 BYOK<br />The most disciplined run so far, with better phase control, quality gates, and manual UX validation.<br /><a href="https://elbruno.com/2026/05/14/github-copilot-cli-squad-gpt-5-5-byok-better-engineering-same-hard-truth/">https://elbruno.com/2026/05/14/github-copilot-cli-squad-gpt-5-5-byok-better-engineering-same-hard-truth/</a></p>



<p class="wp-block-paragraph">The new VS Code 1.122 BYOK flow feels like a natural next step for this series.</p>



<p class="wp-block-paragraph">I haven’t rerun the ElBruno.NetAgent experiment with this new capability yet, but it is definitely on my list.</p>



<p class="wp-block-paragraph">The lesson remains the same:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">The code keeps getting cheaper. Validation and engineering discipline are still expensive.</p>
</blockquote>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/05/30/vs-code-1-122-makes-byok-easier/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40732</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-30-2026-08_49_35-pm.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-30-2026-08_49_35-pm.png">
			<media:title type="html">ChatGPT Image May 30, 2026, 08_49_35 PM</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-30-2026-08_49_35-pm.png?w=1024"/>
	</item>
		<item>
		<title>Building Cross-Framework Agents with MAF, A2A, NVIDIA NeMo, and Aspire</title>
		<link>https://elbruno.com/2026/05/20/building-cross-framework-agents-with-maf-a2a-nvidia-nemo-and-aspire/</link>
					<comments>https://elbruno.com/2026/05/20/building-cross-framework-agents-with-maf-a2a-nvidia-nemo-and-aspire/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Wed, 20 May 2026 15:22:19 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[A2A]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[MAF]]></category>
		<category><![CDATA[Nemo Agent Toolkit]]></category>
		<category><![CDATA[NVidia]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40725</guid>

					<description><![CDATA[Hi! What happens when a Python-based AI agent and a .NET-based AI agent need to work together? That is the idea behind MAF-A2A-NVIDIA-NemoAgents (https://github.com/elbruno/MAF-A2A-NVIDIA-NemoAgents-private): a reference app that shows how to combine NVIDIA NeMo Agent Toolkit, Microsoft Agent Framework, Agent-to-Agent communication, and Aspire into one multi-agent workflow. The repo describes itself as a production-ready sample [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" width="836" height="471" src="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-20-2026-11_20_33-am-small.png?w=836" alt="" class="wp-image-40730" srcset="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-20-2026-11_20_33-am-small.png 836w, https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-20-2026-11_20_33-am-small.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-20-2026-11_20_33-am-small.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-20-2026-11_20_33-am-small.png?w=768 768w" sizes="auto, (max-width: 836px) 100vw, 836px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">What happens when a Python-based AI agent and a .NET-based AI agent need to work together?</p>



<p class="wp-block-paragraph">That is the idea behind <strong><a href="https://github.com/elbruno/MAF-A2A-NVIDIA-NemoAgents-private" target="_blank" rel="noopener">MAF-A2A-NVIDIA-NemoAgents</a></strong> (<a href="https://github.com/elbruno/MAF-A2A-NVIDIA-NemoAgents-private">https://github.com/elbruno/MAF-A2A-NVIDIA-NemoAgents-private</a>): a reference app that shows how to combine <strong>NVIDIA NeMo Agent Toolkit</strong>, <strong>Microsoft Agent Framework</strong>, <strong>Agent-to-Agent communication</strong>, and <strong>Aspire</strong> into one multi-agent workflow. The repo describes itself as a production-ready sample that demonstrates NVIDIA NeMo Agent Toolkit + Microsoft Agent Framework with A2A communication, orchestrated with Aspire.</p>



<p class="wp-block-paragraph">And yes, this is exactly the kind of demo I like: different stacks, different responsibilities, one shared protocol. No “everything must be rewritten in my favorite framework” drama. Just agents talking to agents. Beautiful.</p>



<h2 class="wp-block-heading">The scenario: analysis first, action second</h2>



<p class="wp-block-paragraph">The application models a practical enterprise workflow:</p>



<p class="wp-block-paragraph">A <strong>NeMo Data Analysis Agent</strong> receives business or operational data, analyzes metrics, detects trends or anomalies, and generates insights. Then, when an action is needed, a <strong>MAF Action Agent</strong> can trigger alerts, generate reports, or execute remediation steps. The web UI acts as the human-facing chat surface where users can request analysis and trigger actions.</p>



<p class="wp-block-paragraph">So the flow is simple:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">User  </div><div class="cm-line">↓ Web Chat UI  </div><div class="cm-line">↓ NeMo Data Analysis Agent  </div><div class="cm-line">↓ Analysis result  </div><div class="cm-line">↓ MAF Action Agent  </div><div class="cm-line">↓ Alert / Report / Action</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The interesting part is that these agents are not built with the same framework. The NeMo agent is Python-based. The MAF agent is .NET-based. The bridge between them is <strong>A2A</strong>, using JSON-RPC and agent discovery. The repo architecture describes A2A as the communication layer between the web UI, the NeMo agent, and the MAF agent, with Aspire providing health checks, service discovery, tracing, and startup orchestration.</p>



<h2 class="wp-block-heading">Why two agents?</h2>



<p class="wp-block-paragraph">Because “one mega-agent to rule them all” sounds cool until you need to debug, scale, secure, or explain it.</p>



<p class="wp-block-paragraph">This repo separates responsibilities:</p>



<ul class="wp-block-list">
<li><strong>NeMo Agent</strong>: data analysis, trends, anomalies, metrics, recommendations.</li>



<li><strong>MAF Agent</strong>: actions, alerts, reports, remediation workflows.</li>



<li><strong>Web UI</strong>: chat experience, discovery, orchestration, response rendering.</li>



<li><strong>Aspire</strong>: local distributed app orchestration, health, logs, and OpenTelemetry traces.</li>
</ul>



<p class="wp-block-paragraph">The repo calls out this separation of concerns directly: data analysis expertise is not the same as action execution expertise. It also helps with scalability, reliability, and vendor flexibility.</p>



<h2 class="wp-block-heading">Where A2A fits</h2>



<p class="wp-block-paragraph">A2A is the key enabler here. It gives agents a common way to expose capabilities and communicate across frameworks and languages.</p>



<p class="wp-block-paragraph">On the Microsoft Agent Framework side, A2A support allows agents to be exposed over A2A endpoints and discovered through agent cards. Microsoft’s A2A docs show that Agent Framework can expose multiple agents with separate A2A endpoints, and the <code>agent-framework-a2a</code> package can both connect to external A2A-compliant agents and expose Agent Framework agents over A2A.</p>



<p class="wp-block-paragraph">The .NET A2A pattern looks like this in the official MAF examples:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-variableName">builder</span>.<span class="tok-variableName">AddA2AServer</span>(<span class="tok-string">&quot;weather-agent&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">app</span>.<span class="tok-variableName">MapA2AHttpJson</span>(<span class="tok-string">&quot;weather-agent&quot;</span>, <span class="tok-string">&quot;/a2a/weather-agent&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">app</span>.<span class="tok-variableName">MapWellKnownAgentCard</span>(<span class="tok-keyword">new</span> <span class="tok-variableName">AgentCard</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">Name</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;WeatherAgent&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">Description</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;A helpful weather assistant.&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">SupportedInterfaces</span> <span class="tok-operator">=</span></div><div class="cm-line">    [</div><div class="cm-line">        <span class="tok-keyword">new</span> <span class="tok-variableName">AgentInterface</span></div><div class="cm-line">        {</div><div class="cm-line">            <span class="tok-variableName">Url</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;https://your-host/a2a/weather-agent&quot;</span>,</div><div class="cm-line">            <span class="tok-variableName">ProtocolBinding</span> <span class="tok-operator">=</span> <span class="tok-variableName">ProtocolBindingNames</span>.<span class="tok-variableName">HttpJson</span>,</div><div class="cm-line">            <span class="tok-variableName">ProtocolVersion</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;1.0&quot;</span>,</div><div class="cm-line">        }</div><div class="cm-line">    ]</div><div class="cm-line">});</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That pattern matters because agent discovery is not a nice-to-have. In a multi-agent system, the orchestrator needs to know:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Who are you?</div><div class="cm-line"></div><div class="cm-line">What can you do?</div><div class="cm-line"></div><div class="cm-line">Where do I call you?</div><div class="cm-line"></div><div class="cm-line">Which protocol do you speak?</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That is where the <strong>Agent Card</strong> becomes useful.</p>



<h2 class="wp-block-heading">NeMo as an A2A agent</h2>



<p class="wp-block-paragraph">On the NVIDIA side, NeMo Agent Toolkit can publish workflows as A2A agents. NVIDIA’s docs describe <code>nat a2a serve</code> as the command that starts an A2A server, publishes a workflow as an A2A agent, and exposes an Agent Card at <code>/.well-known/agent-card.json</code>.</p>



<p class="wp-block-paragraph">In this repo, Aspire starts the NeMo workflow using the NeMo Agent Toolkit A2A server approach. The AppHost launches the NeMo agent as an executable, configures the workflow file, host, port, public base URL, NVIDIA API key, Azure OpenAI settings, and OpenTelemetry exporter.</p>



<p class="wp-block-paragraph">A simplified version of that idea looks like this:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">nvidiaApiKey</span> <span class="tok-operator">=</span> <span class="tok-variableName">builder</span>.<span class="tok-variableName">AddParameter</span>(<span class="tok-string">&quot;nvidia-api-key&quot;</span>, <span class="tok-variableName">secret</span>: <span class="tok-atom">true</span>);</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">azureOpenAiEndpoint</span> <span class="tok-operator">=</span> <span class="tok-variableName">builder</span>.<span class="tok-variableName">AddParameter</span>(<span class="tok-string">&quot;azure-openai-endpoint&quot;</span>, <span class="tok-variableName">secret</span>: <span class="tok-atom">true</span>);</div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">azureOpenAiApiKey</span> <span class="tok-operator">=</span> <span class="tok-variableName">builder</span>.<span class="tok-variableName">AddParameter</span>(<span class="tok-string">&quot;azure-openai-api-key&quot;</span>, <span class="tok-variableName">secret</span>: <span class="tok-atom">true</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">nemo</span> <span class="tok-operator">=</span> <span class="tok-variableName">builder</span>.<span class="tok-variableName">AddExecutable</span>(</div><div class="cm-line">        <span class="tok-variableName">name</span>: <span class="tok-string">&quot;nemo-agent&quot;</span>,</div><div class="cm-line">        <span class="tok-variableName">command</span>: <span class="tok-string">&quot;powershell&quot;</span>,</div><div class="cm-line">        <span class="tok-variableName">workingDirectory</span>: <span class="tok-string">&quot;.&quot;</span>,</div><div class="cm-line">        <span class="tok-variableName">args</span>:</div><div class="cm-line">        [</div><div class="cm-line">            <span class="tok-string">&quot;-NoProfile&quot;</span>,</div><div class="cm-line">            <span class="tok-string">&quot;-Command&quot;</span>,</div><div class="cm-line">            <span class="tok-string">&quot;nat a2a serve --config_file .\\src\\NemoDataAnalysisAgent\\nemo\\workflow.yml&quot;</span></div><div class="cm-line">        ])</div><div class="cm-line">    .<span class="tok-variableName">WithHttpEndpoint</span>(<span class="tok-variableName">name</span>: <span class="tok-string">&quot;http&quot;</span>, <span class="tok-variableName">env</span>: <span class="tok-string">&quot;NEMO_PORT&quot;</span>)</div><div class="cm-line">    .<span class="tok-variableName">WithEnvironment</span>(<span class="tok-string">&quot;NEMO_HOST&quot;</span>, <span class="tok-string">&quot;127.0.0.1&quot;</span>)</div><div class="cm-line">    .<span class="tok-variableName">WithEnvironment</span>(<span class="tok-string">&quot;NVIDIA_API_KEY&quot;</span>, <span class="tok-variableName">nvidiaApiKey</span>)</div><div class="cm-line">    .<span class="tok-variableName">WithEnvironment</span>(<span class="tok-string">&quot;AZURE_OPENAI_ENDPOINT&quot;</span>, <span class="tok-variableName">azureOpenAiEndpoint</span>)</div><div class="cm-line">    .<span class="tok-variableName">WithEnvironment</span>(<span class="tok-string">&quot;AZURE_OPENAI_API_KEY&quot;</span>, <span class="tok-variableName">azureOpenAiApiKey</span>)</div><div class="cm-line">    .<span class="tok-variableName">WithOtlpExporter</span>();</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The important idea: <strong>NeMo owns the analysis workflow, but it is exposed as an A2A-compatible service</strong>.</p>



<h2 class="wp-block-heading">MAF as the action agent</h2>



<p class="wp-block-paragraph">The MAF side is a .NET service focused on action execution. The repo exposes endpoints for executing actions, triggering alerts, generating reports, and handling A2A JSON-RPC requests. The README describes the MAF Action Agent as responsible for pluggable action handlers, multi-level alerts, async report generation, health checks, A2A integration, and OpenTelemetry tracing.</p>



<p class="wp-block-paragraph">A simplified version of the MAF action endpoint looks like this:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-variableName">app</span>.<span class="tok-variableName">MapPost</span>(<span class="tok-string">&quot;/api/actions/execute&quot;</span>,</div><div class="cm-line">    <span class="tok-keyword">async</span> (<span class="tok-variableName">ActionRequest</span> <span class="tok-variableName">request</span>, <span class="tok-variableName">IActionExecutor</span> <span class="tok-variableName">executor</span>) <span class="tok-operator">=&gt;</span></div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">result</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">executor</span>.<span class="tok-variableName">ExecuteActionAsync</span>(<span class="tok-variableName">request</span>);</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">Results</span>.<span class="tok-variableName">Ok</span>(<span class="tok-variableName">result</span>);</div><div class="cm-line">    });</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">And the A2A-facing endpoints follow this shape:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-variableName">app</span>.<span class="tok-variableName">MapGet</span>(<span class="tok-string">&quot;/.well-known/agent-card.json&quot;</span>, () <span class="tok-operator">=&gt;</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">Results</span>.<span class="tok-variableName">Ok</span>(<span class="tok-keyword">new</span></div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">name</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;MAF Action Agent&quot;</span>,</div><div class="cm-line">        <span class="tok-variableName">description</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;Executes actions based on NeMo analysis&quot;</span>,</div><div class="cm-line">        <span class="tok-variableName">version</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;1.0.0&quot;</span>,</div><div class="cm-line">        <span class="tok-variableName">capabilities</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span>[]</div><div class="cm-line">        {</div><div class="cm-line">            <span class="tok-string">&quot;execute-actions&quot;</span>,</div><div class="cm-line">            <span class="tok-string">&quot;trigger-alerts&quot;</span>,</div><div class="cm-line">            <span class="tok-string">&quot;generate-reports&quot;</span></div><div class="cm-line">        },</div><div class="cm-line">        <span class="tok-variableName">endpoint</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;/a2a/maf-action-agent&quot;</span>,</div><div class="cm-line">        <span class="tok-variableName">a2a_version</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;1.0&quot;</span></div><div class="cm-line">    });</div><div class="cm-line">});</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">app</span>.<span class="tok-variableName">MapPost</span>(<span class="tok-string">&quot;/a2a/maf-action-agent&quot;</span>,</div><div class="cm-line">    <span class="tok-keyword">async</span> (<span class="tok-variableName">HttpContext</span> <span class="tok-variableName">context</span>, <span class="tok-variableName">IA2ABridge</span> <span class="tok-variableName">bridge</span>) <span class="tok-operator">=&gt;</span></div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">reader</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">StreamReader</span>(<span class="tok-variableName">context</span>.<span class="tok-variableName">Request</span>.<span class="tok-variableName">Body</span>);</div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">body</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">reader</span>.<span class="tok-variableName">ReadToEndAsync</span>();</div><div class="cm-line"></div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">response</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">bridge</span>.<span class="tok-variableName">ProcessA2ARequestAsync</span>(<span class="tok-variableName">body</span>);</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">Results</span>.<span class="tok-variableName">Text</span>(<span class="tok-variableName">response</span>, <span class="tok-string">&quot;application/json&quot;</span>);</div><div class="cm-line">    });</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That is the handoff: the agent advertises what it can do, then receives structured requests through the A2A endpoint.</p>



<h2 class="wp-block-heading">The web UI as the orchestrator</h2>



<p class="wp-block-paragraph">The web chat interface is where the human interacts with the system. It discovers both agents, sends analysis requests to NeMo, and optionally sends action requests to MAF. The README calls out a <strong>NeMo-first routing</strong> pattern: chat responses come from NeMo analysis before optional MAF action execution.</p>



<p class="wp-block-paragraph">A simplified orchestration pattern looks like this:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">public</span> <span class="tok-keyword">async</span> <span class="tok-typeName">Task</span><span class="tok-operator">&lt;</span><span class="tok-variableName">ChatResponse</span><span class="tok-operator">&gt;</span> <span class="tok-variableName tok-definition">ProcessChatAsync</span>(<span class="tok-variableName">ChatRequest</span> <span class="tok-variableName">request</span>)</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">message</span> <span class="tok-operator">=</span> <span class="tok-variableName">request</span>.<span class="tok-variableName">Message</span>.<span class="tok-variableName">ToLowerInvariant</span>();</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-variableName">ShouldTriggerAction</span>(<span class="tok-variableName">message</span>))</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">action</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ActionRequest</span></div><div class="cm-line">        {</div><div class="cm-line">            <span class="tok-variableName">ActionType</span> <span class="tok-operator">=</span> <span class="tok-variableName">InferActionType</span>(<span class="tok-variableName">message</span>),</div><div class="cm-line">            <span class="tok-variableName">Parameters</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">Dictionary</span><span class="tok-operator">&lt;</span><span class="tok-typeName">string</span>, <span class="tok-typeName">object</span><span class="tok-operator">?&gt;</span></div><div class="cm-line">            {</div><div class="cm-line">                [<span class="tok-string">&quot;message&quot;</span>] <span class="tok-operator">=</span> <span class="tok-variableName">request</span>.<span class="tok-variableName">Message</span></div><div class="cm-line">            }</div><div class="cm-line">        };</div><div class="cm-line"></div><div class="cm-line">        <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">actionResult</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">orchestrator</span>.<span class="tok-variableName">ExecuteActionAsync</span>(<span class="tok-variableName">action</span>);</div><div class="cm-line"></div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ChatResponse</span></div><div class="cm-line">        {</div><div class="cm-line">            <span class="tok-variableName">RespondedBy</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;MAF Action Agent&quot;</span>,</div><div class="cm-line">            <span class="tok-variableName">Content</span> <span class="tok-operator">=</span> <span class="tok-variableName">actionResult</span>.<span class="tok-variableName">Details</span></div><div class="cm-line">        };</div><div class="cm-line">    }</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">nemoReply</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">orchestrator</span>.<span class="tok-variableName">SendNemoMessageAsync</span>(</div><div class="cm-line">        <span class="tok-variableName">request</span>.<span class="tok-variableName">Message</span>,</div><div class="cm-line">        <span class="tok-variableName">request</span>.<span class="tok-variableName">SessionId</span>);</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ChatResponse</span></div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">RespondedBy</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;NeMo Data Analysis Agent&quot;</span>,</div><div class="cm-line">        <span class="tok-variableName">Content</span> <span class="tok-operator">=</span> <span class="tok-variableName">nemoReply</span></div><div class="cm-line">    };</div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That is a nice pattern for demos and real apps: default to insight, escalate to action only when the user explicitly asks for it.</p>



<h2 class="wp-block-heading">A2A request shape</h2>



<p class="wp-block-paragraph">The architecture docs describe JSON-RPC 2.0 over HTTP as the protocol used between agents. The documented example sends a method such as <code>analyze</code>, passes parameters like metric, timeframe, and aggregation, and receives a structured result with trend, anomalies, forecast, and insights.</p>



<p class="wp-block-paragraph">A simplified A2A-style request could look like this:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-json"><div class="cm-line"><span class="tok-punctuation">{</span></div><div class="cm-line">  <span class="tok-propertyName">&quot;jsonrpc&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;2.0&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">  <span class="tok-propertyName">&quot;method&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;message/send&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">  <span class="tok-propertyName">&quot;params&quot;</span><span class="tok-punctuation">:</span> <span class="tok-punctuation">{</span></div><div class="cm-line">    <span class="tok-propertyName">&quot;message&quot;</span><span class="tok-punctuation">:</span> <span class="tok-punctuation">{</span></div><div class="cm-line">      <span class="tok-propertyName">&quot;kind&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;message&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">      <span class="tok-propertyName">&quot;role&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;user&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">      <span class="tok-propertyName">&quot;parts&quot;</span><span class="tok-punctuation">:</span> <span class="tok-punctuation">[</span></div><div class="cm-line">        <span class="tok-punctuation">{</span></div><div class="cm-line">          <span class="tok-propertyName">&quot;kind&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;text&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">          <span class="tok-propertyName">&quot;text&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;Analyze quarterly revenue trends&quot;</span></div><div class="cm-line">        <span class="tok-punctuation">}</span></div><div class="cm-line">      <span class="tok-punctuation">]</span></div><div class="cm-line">    <span class="tok-punctuation">}</span></div><div class="cm-line">  <span class="tok-punctuation">}</span><span class="tok-punctuation">,</span></div><div class="cm-line">  <span class="tok-propertyName">&quot;id&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;request-001&quot;</span></div><div class="cm-line"><span class="tok-punctuation">}</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">And the system can route that to the NeMo agent first. Later, a second request can trigger the MAF agent:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-json"><div class="cm-line"><span class="tok-punctuation">{</span></div><div class="cm-line">  <span class="tok-propertyName">&quot;actionType&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;trigger-alert&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">  <span class="tok-propertyName">&quot;parameters&quot;</span><span class="tok-punctuation">:</span> <span class="tok-punctuation">{</span></div><div class="cm-line">    <span class="tok-propertyName">&quot;message&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;Trigger alert for high CPU usage&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-propertyName">&quot;analysisSummary&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;CPU usage increased 35% over baseline.&quot;</span></div><div class="cm-line">  <span class="tok-punctuation">}</span></div><div class="cm-line"><span class="tok-punctuation">}</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That second part is where things become useful. The action agent is not acting in isolation; it can act based on the previous analysis.</p>



<h2 class="wp-block-heading">Why Aspire matters here</h2>



<p class="wp-block-paragraph">Aspire is doing a lot of the boring-but-important work.</p>



<p class="wp-block-paragraph">The repo uses Aspire to coordinate startup order, service discovery, health checks, logging, and OpenTelemetry correlation across the NeMo agent, the MAF agent, and the web UI. The README describes Aspire as responsible for automatic agent endpoint registration, dependency management, continuous health checks, distributed tracing, unified logs, and NeMo pre-warm.</p>



<p class="wp-block-paragraph">This is one of the hidden wins of the repo. Multi-agent demos often fail because they are hard to run. Here, Aspire gives the app a “one command, multiple services” developer experience:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">aspire start</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The README says that after starting the app, the web UI is available on port <code>5000</code>, and the sample prompts include NeMo-only analysis, MAF-only alert triggering, and a combined NeMo + MAF workflow.</p>



<h2 class="wp-block-heading">Why this repo is interesting</h2>



<p class="wp-block-paragraph">This repo is not just “another chat app.”</p>



<p class="wp-block-paragraph">It demonstrates a few patterns that matter for real enterprise AI systems:</p>



<ol class="wp-block-list">
<li><strong>Cross-framework agents</strong><br />Python and .NET agents can collaborate without forcing everything into one stack.</li>



<li><strong>Clear agent responsibility boundaries</strong><br />One agent analyzes. One agent acts. That makes the architecture easier to explain, test, and evolve.</li>



<li><strong>A2A as the interoperability layer</strong><br />A2A gives each agent a common communication contract.</li>



<li><strong>Aspire as the local cloud-native dev loop</strong><br />Developers can run, observe, and debug the system locally before thinking about containers or cloud deployment.</li>



<li><strong>Observability from the beginning</strong><br />The architecture includes OpenTelemetry tracing, structured logging, and health checks instead of treating them as “we’ll add this later” work. The repo’s architecture highlights call out distributed tracing, resilience patterns, scalability, and a future roadmap for TLS, mutual authentication, and authorization.</li>
</ol>



<h2 class="wp-block-heading">Final thoughts</h2>



<p class="wp-block-paragraph">The big takeaway for me: <strong>A2A makes heterogeneous agent systems feel much more realistic</strong>.</p>



<p class="wp-block-paragraph">In real organizations, not every team will use the same language, same framework, same model provider, or same runtime. Some teams will build in Python. Some will build in .NET. Some will use NVIDIA tooling. Some will use Microsoft Agent Framework. Some will deploy locally first, then move to Azure.</p>



<p class="wp-block-paragraph">And that is fine.</p>



<p class="wp-block-paragraph">The goal is not to force every agent into one framework. The goal is to give them a shared way to discover each other, communicate, and collaborate.</p>



<p class="wp-block-paragraph">That is what this repo demonstrates: <strong>NVIDIA NeMo for analysis, MAF for action, A2A for interoperability, and Aspire for orchestration</strong>.</p>



<p class="wp-block-paragraph">Agents talking to agents.</p>



<p class="wp-block-paragraph">Finally, in a way we can actually run, observe, and explain.</p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/05/20/building-cross-framework-agents-with-maf-a2a-nvidia-nemo-and-aspire/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40725</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-20-2026-11_20_33-am-small.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-20-2026-11_20_33-am-small.png">
			<media:title type="html">ChatGPT Image May 20, 2026, 11_20_33 AM-small</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-20-2026-11_20_33-am-small.png?w=836"/>
	</item>
		<item>
		<title>GitHub Copilot CLI + SQUAD + GPT-5.5 BYOK: Better Engineering, Same Hard Truth</title>
		<link>https://elbruno.com/2026/05/14/github-copilot-cli-squad-gpt-5-5-byok-better-engineering-same-hard-truth/</link>
					<comments>https://elbruno.com/2026/05/14/github-copilot-cli-squad-gpt-5-5-byok-better-engineering-same-hard-truth/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Thu, 14 May 2026 13:28:46 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[GitHub Copilot]]></category>
		<category><![CDATA[GitHub Copilot CLI]]></category>
		<category><![CDATA[GPT-5.5]]></category>
		<category><![CDATA[LLM]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40713</guid>

					<description><![CDATA[TL;DR This was the final ElBruno.NetAgent experiment: GitHub Copilot CLI + SQUAD using Azure OpenAI GPT-5.5 BYOK against the same app-building challenge I previously tried with CPU-only local models, GPU local models, and GPT-5-mini. The good news: GPT-5.5 was clearly better at staying inside phase boundaries, following safety rules, reducing broad stabilization loops, and working [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<figure class="wp-block-image size-large"><img loading="lazy" width="836" height="471" src="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-14-2026-09_11_31-am-small.png?w=836" alt="" class="wp-image-40714" srcset="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-14-2026-09_11_31-am-small.png 836w, https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-14-2026-09_11_31-am-small.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-14-2026-09_11_31-am-small.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-14-2026-09_11_31-am-small.png?w=768 768w" sizes="auto, (max-width: 836px) 100vw, 836px" /></figure>



<h2 class="wp-block-heading">TL;DR</h2>



<p class="wp-block-paragraph">This was the final ElBruno.NetAgent experiment: GitHub Copilot CLI + SQUAD using Azure OpenAI GPT-5.5 BYOK against the same app-building challenge I previously tried with CPU-only local models, GPU local models, and GPT-5-mini.</p>



<p class="wp-block-paragraph">The good news: GPT-5.5 was clearly better at staying inside phase boundaries, following safety rules, reducing broad stabilization loops, and working with strict quality gates. Once I switched to economy-mode prompts, the flow became much more disciplined: usually one implementation agent, one reviewer, short loops, and no wild repo-wide rewrites.</p>



<p class="wp-block-paragraph">The not-so-funny news: better models do not remove the need for engineering discipline. Build green, tests green, and smoke tests green still missed a real manual UX bug: the Settings UI looked like it saved Auto Mode, but the persisted config did not actually keep the value. The fix was not “more tests”; it was the right test: one that exercised the real WPF user path, not only the ViewModel.</p>



<p class="wp-block-paragraph">Also: GPT-5.5 BYOK is powerful, but not cheap. By the time the app reached final automated validation, the run was already around 15M+ tokens and roughly $30+ in Azure OpenAI usage. SQUAD is useful, but every agent has a context tax.</p>



<p class="wp-block-paragraph">The biggest lesson: the code is still cheap. The validation strategy is the expensive part.</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="688" height="384" src="https://elbruno.com/wp-content/uploads/2026/05/img01-small.png?w=688" alt="" class="wp-image-40717" srcset="https://elbruno.com/wp-content/uploads/2026/05/img01-small.png 688w, https://elbruno.com/wp-content/uploads/2026/05/img01-small.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/img01-small.png?w=300 300w" sizes="auto, (max-width: 688px) 100vw, 688px" /></figure>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The Setup</h2>



<p class="wp-block-paragraph">Main repo:</p>



<ul class="wp-block-list">
<li><a href="https://github.com/elbruno/ElBruno.NetAgent">https://github.com/elbruno/ElBruno.NetAgent</a></li>
</ul>



<p class="wp-block-paragraph">Previous experiments:</p>



<ol class="wp-block-list">
<li>CPU-only local model: <a href="https://elbruno.com/2026/05/03/running-github-copilot-cli-offline-with-local-models-a-cpu-only-reality-check/">https://elbruno.com/2026/05/03/running-github-copilot-cli-offline-with-local-models-a-cpu-only-reality-check/</a></li>



<li>GPU local model: <a href="https://elbruno.com/2026/05/06/running-github-copilot-cli-offline-with-local-models-gpu-edition/">https://elbruno.com/2026/05/06/running-github-copilot-cli-offline-with-local-models-gpu-edition/</a></li>



<li>GPT-5-mini BYOK: <a href="https://elbruno.com/2026/05/11/github-copilot-cli-gpt-5-mini-byok-the-code-was-cheap-the-quality-gates-were-expensive/">https://elbruno.com/2026/05/11/github-copilot-cli-gpt-5-mini-byok-the-code-was-cheap-the-quality-gates-were-expensive/</a></li>
</ol>



<p class="wp-block-paragraph">This time the goal was simple:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Run the same ElBruno.NetAgent app-building experiment using GitHub Copilot CLI + SQUAD + Azure OpenAI GPT-5.5 BYOK and compare stabilization loops against GPT-5-mini.</p>
</blockquote>



<p class="wp-block-paragraph">The stack:</p>



<ul class="wp-block-list">
<li>GitHub Copilot CLI</li>



<li>SQUAD orchestration</li>



<li>Azure OpenAI GPT-5.5 BYOK</li>



<li>.NET + WPF</li>



<li>Windows tray app</li>



<li>Strict quality gates</li>



<li>Manual UX validation</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<figure class="wp-block-image size-large"><img loading="lazy" width="688" height="384" src="https://elbruno.com/wp-content/uploads/2026/05/gem02-small.png?w=688" alt="" class="wp-image-40723" srcset="https://elbruno.com/wp-content/uploads/2026/05/gem02-small.png 688w, https://elbruno.com/wp-content/uploads/2026/05/gem02-small.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/gem02-small.png?w=300 300w" sizes="auto, (max-width: 688px) 100vw, 688px" /></figure>



<h2 class="wp-block-heading">Step 0: Define “Working” Before Writing Code</h2>



<p class="wp-block-paragraph">One of the biggest lessons from the GPT-5-mini run was that “build green” was nowhere near enough.</p>



<p class="wp-block-paragraph">So before writing any product code, the experiment started with a dedicated Step 0 phase:</p>



<ul class="wp-block-list">
<li>Normalize SQUAD routing to GPT-5.5</li>



<li>Define team rules</li>



<li>Define safety guardrails</li>



<li>Define quality gates</li>



<li>Define manual UX validation</li>



<li>Prevent uncontrolled multi-agent fan-out</li>
</ul>



<p class="wp-block-paragraph">This turned out to be one of the smartest decisions in the whole experiment.</p>



<p class="wp-block-paragraph">The model was better, but the stronger engineering structure mattered even more.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The Phase-Based Build</h2>



<p class="wp-block-paragraph">Instead of asking the model to “build the app,” the project was split into very strict phases:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Phase</th><th class="has-text-align-left" data-align="left">Goal</th></tr></thead><tbody><tr><td class="has-text-align-left" data-align="left">Phase 1</td><td class="has-text-align-left" data-align="left">Minimal .NET solution skeleton</td></tr><tr><td class="has-text-align-left" data-align="left">Phase 2</td><td class="has-text-align-left" data-align="left">Core options, services, DI, safe defaults</td></tr><tr><td class="has-text-align-left" data-align="left">Phase 3</td><td class="has-text-align-left" data-align="left">WPF shell and smoke-testable windows</td></tr><tr><td class="has-text-align-left" data-align="left">Phase 4</td><td class="has-text-align-left" data-align="left">Editable Settings behavior</td></tr><tr><td class="has-text-align-left" data-align="left">Phase 5</td><td class="has-text-align-left" data-align="left">Read-only Network Selector and dry-run logic</td></tr><tr><td class="has-text-align-left" data-align="left">Phase 6</td><td class="has-text-align-left" data-align="left">Tray shell, Help/About, clean exit</td></tr><tr><td class="has-text-align-left" data-align="left">Phase 7</td><td class="has-text-align-left" data-align="left">Status view and product polish</td></tr><tr><td class="has-text-align-left" data-align="left">Phase 8</td><td class="has-text-align-left" data-align="left">Final automated validation</td></tr><tr><td class="has-text-align-left" data-align="left">Phase 9</td><td class="has-text-align-left" data-align="left">Manual UX bug fixing</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">This worked much better than broad “vibe coding.”</p>



<p class="wp-block-paragraph">GPT-5.5 was noticeably better at respecting:</p>



<ul class="wp-block-list">
<li>phase boundaries</li>



<li>“do not implement” instructions</li>



<li>safety rules</li>



<li>dry-run requirements</li>



<li>deterministic testing constraints</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Economy Mode Was the Real Unlock</h2>



<p class="wp-block-paragraph">Early SQUAD runs were expensive and noisy.</p>



<p class="wp-block-paragraph">Multiple agents reading large repo context repeatedly created a huge token tax.</p>



<p class="wp-block-paragraph">The biggest improvement came after switching to what I started calling “economy mode”:</p>



<ul class="wp-block-list">
<li>one implementation agent</li>



<li>one reviewer</li>



<li>no Scribe unless needed</li>



<li>no doc rewrites</li>



<li>no template rewrites</li>



<li>read only files needed for the phase</li>



<li>small targeted edits</li>



<li>stop after the phase</li>
</ul>



<p class="wp-block-paragraph">This dramatically reduced chaos and stabilization loops.</p>



<p class="wp-block-paragraph">Ironically, the better the model became, the more important project-management-style prompting became.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<figure class="wp-block-image size-large"><img loading="lazy" width="688" height="384" src="https://elbruno.com/wp-content/uploads/2026/05/gem06-small.png?w=688" alt="" class="wp-image-40718" srcset="https://elbruno.com/wp-content/uploads/2026/05/gem06-small.png 688w, https://elbruno.com/wp-content/uploads/2026/05/gem06-small.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/gem06-small.png?w=300 300w" sizes="auto, (max-width: 688px) 100vw, 688px" /></figure>



<h2 class="wp-block-heading">The Cost Checkpoint</h2>



<p class="wp-block-paragraph">By the time the app reached final automated validation:</p>



<ul class="wp-block-list">
<li>~15.5M tokens consumed</li>



<li>400+ requests</li>



<li>~$30+ estimated Azure OpenAI cost</li>
</ul>



<p class="wp-block-paragraph">Most of the usage was input/context tokens, not output tokens.</p>



<p class="wp-block-paragraph">That’s an important lesson:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">SQUAD adds value, but every agent carries context overhead.</p>
</blockquote>



<p class="wp-block-paragraph">GPT-5.5 BYOK is powerful, but it is definitely not “free vibe coding.”</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The Quality Gates Worked… Until They Didn’t</h2>



<p class="wp-block-paragraph">The automated gate looked solid:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet clean .\ElBruno.NetAgent.sln</div><div class="cm-line">dotnet build .\ElBruno.NetAgent.sln -c Release</div><div class="cm-line">dotnet test .\ElBruno.NetAgent.sln -c Release --no-build --settings disable-parallel.runsettings</div><div class="cm-line">dotnet run --project src\ElBruno.NetAgent -- --smoke-test</div><div class="cm-line">dotnet run --project src\ElBruno.NetAgent -- --smoke-test-exit</div><div class="cm-line">dotnet run --project src\ElBruno.NetAgent -- --smoke-test-settings-save</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">And honestly, GPT-5.5 handled this surprisingly well.</p>



<p class="wp-block-paragraph">The project reached:</p>



<ul class="wp-block-list">
<li>39+ passing tests</li>



<li>fast deterministic runs</li>



<li>smoke-test validation</li>



<li>WPF window construction validation</li>



<li>tray exit validation</li>



<li>forbidden-command safety scans</li>
</ul>



<p class="wp-block-paragraph">But then manual UX validation found a real bug.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The Manual UX Bug That Mattered</h2>



<p class="wp-block-paragraph">Manual testing showed:</p>



<ul class="wp-block-list">
<li>tray menu looked OK</li>



<li>Help worked</li>



<li>Exit worked</li>



<li>Settings UI looked rough</li>



<li>Save did not persist Auto Mode</li>
</ul>



<p class="wp-block-paragraph">At first this was confusing because:</p>



<ul class="wp-block-list">
<li>tests were green</li>



<li>smoke tests were green</li>



<li>SettingsViewModel tests passed</li>
</ul>



<p class="wp-block-paragraph">The problem was deeper: the tests validated the ViewModel, but not the real user path.</p>



<p class="wp-block-paragraph">The real failure path was:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">UI checkbox</div><div class="cm-line">→ Save and Close button</div><div class="cm-line">→ persisted config file</div><div class="cm-line">→ reload</div><div class="cm-line">→ UI state</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The fix was not “more tests.”</p>



<p class="wp-block-paragraph">The fix was:</p>



<ul class="wp-block-list">
<li>testing the actual WPF interaction path</li>



<li>validating persisted config values</li>



<li>validating reload behavior</li>



<li>validating the same SaveAndCloseAsync() path used by the real button</li>
</ul>



<p class="wp-block-paragraph">That was probably the most important lesson of the entire experiment.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">The right test is the one that fails for the same reason your user fails.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">What GPT-5.5 Did Better Than GPT-5-mini</h2>



<p class="wp-block-paragraph">GPT-5.5 was clearly better at:</p>



<ul class="wp-block-list">
<li>phase discipline</li>



<li>respecting safety boundaries</li>



<li>avoiding repo-wide chaos</li>



<li>using abstractions/fakes</li>



<li>isolating WPF/tray behavior</li>



<li>deterministic testing patterns</li>



<li>targeted fixes</li>



<li>reviewer coordination</li>
</ul>



<p class="wp-block-paragraph">The tray + exit phase was especially interesting.</p>



<p class="wp-block-paragraph">GPT-5-mini had previously struggled around:</p>



<ul class="wp-block-list">
<li>NotifyIcon behavior</li>



<li>WPF dispatcher behavior</li>



<li>tray shutdown</li>



<li>lingering processes</li>
</ul>



<p class="wp-block-paragraph">GPT-5.5 handled this much more cleanly once the prompts explicitly enforced:</p>



<ul class="wp-block-list">
<li>fake adapters</li>



<li>no native tray tests</li>



<li>no real dispatcher loops</li>



<li>deterministic test behavior</li>
</ul>



<p class="wp-block-paragraph">But stronger models still did not remove the need for:</p>



<ul class="wp-block-list">
<li>manual UX testing</li>



<li>safety reviews</li>



<li>persistence validation</li>



<li>engineering checkpoints</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">What I Would Do Differently Next Time</h2>



<p class="wp-block-paragraph">A few practical lessons:</p>



<ul class="wp-block-list">
<li>Start with economy mode immediately</li>



<li>Use one implementer + one reviewer by default</li>



<li>Treat UI-path tests as mandatory</li>



<li>Validate persisted config files directly</li>



<li>Manual UX validation should happen earlier</li>



<li>Smoke tests should validate real user flows</li>



<li>Track token usage per phase</li>



<li>Keep phases narrow and explicit</li>
</ul>



<p class="wp-block-paragraph">Most importantly:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Define what “working” means before generating code.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<figure class="wp-block-image size-large"><img loading="lazy" width="688" height="384" src="https://elbruno.com/wp-content/uploads/2026/05/gem08-small.png?w=688" alt="" class="wp-image-40720" srcset="https://elbruno.com/wp-content/uploads/2026/05/gem08-small.png 688w, https://elbruno.com/wp-content/uploads/2026/05/gem08-small.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/gem08-small.png?w=300 300w" sizes="auto, (max-width: 688px) 100vw, 688px" /></figure>



<h2 class="wp-block-heading">Final Thoughts</h2>



<p class="wp-block-paragraph">This experiment ended in a much better place than the GPT-5-mini run.</p>



<p class="wp-block-paragraph">The stabilization loops were smaller. The orchestration was cleaner. The engineering flow was more controlled.</p>



<p class="wp-block-paragraph">But the biggest lesson did not change.</p>



<p class="wp-block-paragraph">The hard part was never generating code.</p>



<p class="wp-block-paragraph">The hard part was:</p>



<ul class="wp-block-list">
<li>defining quality</li>



<li>validating behavior</li>



<li>testing real user flows</li>



<li>controlling orchestration</li>



<li>knowing when “green” was lying</li>
</ul>



<p class="wp-block-paragraph">GPT-5.5 made the engineering loop better.</p>



<p class="wp-block-paragraph">It did not eliminate the need for engineering discipline.</p>



<p class="wp-block-paragraph">And that is probably the most important AI-assisted development lesson I’ve learned so far:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Today, the code is cheap.<br />The decisions, validation, and engineering discipline are still expensive.</p>
</blockquote>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/05/14/github-copilot-cli-squad-gpt-5-5-byok-better-engineering-same-hard-truth/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40713</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-14-2026-09_11_31-am-small.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-14-2026-09_11_31-am-small.png">
			<media:title type="html">ChatGPT Image May 14, 2026, 09_11_31 AM-small</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-14-2026-09_11_31-am-small.png?w=836"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/img01-small.png?w=688"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/gem02-small.png?w=688"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/gem06-small.png?w=688"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/gem08-small.png?w=688"/>
	</item>
		<item>
		<title>GitHub Copilot CLI + GPT-5-mini BYOK: The Code Was Cheap, the Quality Gates Were Expensive</title>
		<link>https://elbruno.com/2026/05/11/github-copilot-cli-gpt-5-mini-byok-the-code-was-cheap-the-quality-gates-were-expensive/</link>
					<comments>https://elbruno.com/2026/05/11/github-copilot-cli-gpt-5-mini-byok-the-code-was-cheap-the-quality-gates-were-expensive/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Mon, 11 May 2026 13:38:01 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[BYOK]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[GitHub Copilot CLI]]></category>
		<category><![CDATA[gpt-5-mini]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[technology]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40317</guid>

					<description><![CDATA[The first screenshot shows GitHub Copilot CLI ready with the SQUAD agent and GPT-5-mini selected. Then SQUAD started spawning background agents.]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" width="688" height="384" src="https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_vpj2hyvpj2hyvpj2-small.png?w=688" alt="" class="wp-image-40328" srcset="https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_vpj2hyvpj2hyvpj2-small.png 688w, https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_vpj2hyvpj2hyvpj2-small.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_vpj2hyvpj2hyvpj2-small.png?w=300 300w" sizes="auto, (max-width: 688px) 100vw, 688px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<div class="wp-block-jetpack-markdown"><h2>TL;DR</h2>
<p>This is part 3 of my GitHub Copilot CLI + BYOK experiment series.</p>
<p>After testing local models on CPU and GPU, I moved the same experiment to Azure OpenAI using GPT-5-mini through BYOK. Same repo, same goal, same safety constraints — but this time with a cloud model that made SQUAD practical again.</p>
<p>And that was the first big win.</p>
<p>With local models, the workflow was possible but fragile. With GPT-5-mini, GitHub Copilot CLI could coordinate SQUAD agents, run background tasks, create commits, fix issues, and move the app forward in a way that finally felt like real agent orchestration.</p>
<p>The result: ElBruno.NetAgent became a working Windows tray app for safe, dry-run network selection assistance.</p>
<p>But the real lesson was not “GPT-5-mini can generate code.”</p>
<p>The real lesson was this:</p>
<blockquote>
<p>The code was cheap. The quality gates were expensive.</p>
</blockquote>
<p>Build green was not enough. Tests green was not enough. Even smoke-test green was not enough at first.</p>
<p>We had to keep expanding the definition of “working” until it included runtime startup, dependency injection, WPF/XAML loading, tray lifecycle, fast deterministic tests, editable settings, save without freezing, and manual UX validation.</p>
</div>



<div class="wp-block-jetpack-markdown"><h1>The whole GPT-5-mini story</h1>
<h2>The experiment so far</h2>
<p>This is part 3 of the experiment series.</p>
<p>In the previous posts, I tested GitHub Copilot CLI with local models:</p>
<ul>
<li>CPU-only local model experiment: <a href="https://elbruno.com/2026/05/03/running-github-copilot-cli-offline-with-local-models-a-cpu-only-reality-check/">https://elbruno.com/2026/05/03/running-github-copilot-cli-offline-with-local-models-a-cpu-only-reality-check/</a></li>
<li>GPU local model experiment: <a href="https://elbruno.com/2026/05/06/running-github-copilot-cli-offline-with-local-models-gpu-edition/">https://elbruno.com/2026/05/06/running-github-copilot-cli-offline-with-local-models-gpu-edition/</a></li>
</ul>
<p>The repo for this experiment is here:</p>
<p><a href="https://github.com/elbruno/ElBruno.NetAgent" rel="nofollow">https://github.com/elbruno/ElBruno.NetAgent</a></p>
<p>And the GPT-5-mini work happened in this PR:</p>
<p><a href="https://github.com/elbruno/ElBruno.NetAgent/pull/3" rel="nofollow">https://github.com/elbruno/ElBruno.NetAgent/pull/3</a></p>
<p>The full series is about testing what can and cannot be done with GitHub Copilot CLI, SQUAD, and BYOK across different model setups:</p>
<ul>
<li>local models on CPU</li>
<li>local models on GPU</li>
<li>cloud model with GPT-5-mini</li>
<li>cloud model with GPT-5.5</li>
</ul>
<p>The final GPT-5.5 experiment is next. But before jumping there, this GPT-5-mini run deserves its own write-up, because it showed something very interesting.</p>
<p>The model was good enough to generate a lot of code.</p>
<p>But the hard part was not generating code.</p>
<p>The hard part was defining what “working” actually means.</p>
<h2>The setup</h2>
<p>The goal was to start from a clean repo and build a real app, not just a toy sample.</p>
<p>The app is ElBruno.NetAgent: a Windows tray app that helps with safe, dry-run network selection assistance.</p>
<p>The important phrase here is <strong>dry-run</strong>.</p>
<p>This app must not change Windows network settings. It must not enable or disable adapters. It must not run <code>netsh</code>, <code>Set-NetAdapter</code>, <code>Enable-NetAdapter</code>, <code>Disable-NetAdapter</code>, or anything similar.</p>
<p>The safety defaults were clear from the beginning:</p>
<ul>
<li><code>DryRunMode=true</code></li>
<li><code>AutoModeEnabled=false</code></li>
<li>no real network mutation</li>
<li>no admin/elevation requirement</li>
<li>no secrets</li>
<li>no NuGet publish unless explicitly requested</li>
</ul>
<p>For the GitHub Copilot CLI setup, the important part was using Azure OpenAI through BYOK and selecting GPT-5-mini.</p>
<p>Example shape of the setup:</p>
<pre><code class="language-powershell">$env:COPILOT_PROVIDER_TYPE=&quot;azure&quot;
$env:COPILOT_PROVIDER_BASE_URL=&quot;https://&lt;resource&gt;.openai.azure.com/openai/deployments/&lt;deployment&gt;&quot;
$env:COPILOT_PROVIDER_API_KEY=&quot;&lt;key&gt;&quot;
$env:COPILOT_PROVIDER_WIRE_API=&quot;responses&quot;
$env:COPILOT_MODEL=&quot;gpt-5-mini&quot;

$env:COPILOT_PROVIDER_MAX_PROMPT_TOKENS=&quot;32768&quot;
$env:COPILOT_PROVIDER_MAX_OUTPUT_TOKENS=&quot;1536&quot;

copilot --agent squad --max-autopilot-continues 6
</code></pre>
<p>No keys in the repo, no secrets in the post, obviously.</p>
<p>The branch used for this run was:</p>
<pre><code class="language-text">gpt5miniGeneratedApp
</code></pre>
</div>



<div class="wp-block-jetpack-markdown"><h2>The first big difference: SQUAD worked</h2>
<p>This was the first major difference compared with the local model experiments.</p>
<p>With local models, SQUAD was either not practical, too slow, or fragile. The flow needed very small prompts, tight phases, and a lot of human checkpointing.</p>
<p>With GPT-5-mini BYOK, background agents started working.</p>
<p>That changed the whole experience.</p>
</div>



<figure class="wp-block-image size-large"><img loading="lazy" width="756" height="361" src="https://elbruno.com/wp-content/uploads/2026/05/image-1.png?w=756" alt="" class="wp-image-40321" srcset="https://elbruno.com/wp-content/uploads/2026/05/image-1.png 756w, https://elbruno.com/wp-content/uploads/2026/05/image-1.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/image-1.png?w=300 300w" sizes="auto, (max-width: 756px) 100vw, 756px" /></figure>



<p class="wp-block-paragraph">The first screenshot shows GitHub Copilot CLI ready with the SQUAD agent and GPT-5-mini selected.</p>



<p class="wp-block-paragraph">Then SQUAD started spawning background agents.</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="759" height="366" src="https://elbruno.com/wp-content/uploads/2026/05/image-2.png?w=759" alt="" class="wp-image-40322" srcset="https://elbruno.com/wp-content/uploads/2026/05/image-2.png 759w, https://elbruno.com/wp-content/uploads/2026/05/image-2.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/image-2.png?w=300 300w" sizes="auto, (max-width: 759px) 100vw, 759px" /></figure>



<div class="wp-block-jetpack-markdown"><p>This is where the experiment started to feel different.</p>
<p>With local models, I was mostly trying to keep the workflow alive.</p>
<p>With GPT-5-mini, I could ask SQUAD to coordinate work, run agents in the background, and move from phase to phase.</p>
<p>It finally felt like agent orchestration instead of prompt babysitting.</p>
<h2>What the app became</h2>
<p>At the end of the GPT-5-mini run, ElBruno.NetAgent became a working Windows tray app.</p>
<p>The app can:</p>
<ul>
<li>start from <code>dotnet run --project src/ElBruno.NetAgent</code></li>
<li>show a tray icon</li>
<li>open a Status window</li>
<li>open a Network Selector window</li>
<li>open an editable Settings window</li>
<li>preview the best network switch in dry-run mode</li>
<li>expose a Help/About area with the GitHub repo link</li>
<li>save settings without freezing</li>
<li>exit cleanly and return the PowerShell prompt</li>
</ul>
</div>



<figure class="wp-block-image size-large"><img loading="lazy" width="711" height="452" src="https://elbruno.com/wp-content/uploads/2026/05/image-3.png?w=711" alt="" class="wp-image-40324" srcset="https://elbruno.com/wp-content/uploads/2026/05/image-3.png 711w, https://elbruno.com/wp-content/uploads/2026/05/image-3.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/image-3.png?w=300 300w" sizes="auto, (max-width: 711px) 100vw, 711px" /></figure>



<div class="wp-block-jetpack-markdown"><p>The Network Selector shows detected interfaces, their kind, status, and quality information. It is still intentionally safe: no real adapter changes are performed.</p>
<p>That is important.</p>
<p>The app is not a “change my network settings automatically” app. It is a safe assistant that can inspect, score, recommend, and dry-run.</p>
<h2>Where things broke</h2>
<p>This is where the experiment became really valuable.</p>
<p>GPT-5-mini + SQUAD generated a lot of useful code, but the app did not become “working” in a single pass.</p>
<p>Every time we improved the quality gate, we found a new kind of bug.</p>
<p>Here are the most important ones.</p>
<table>
<thead>
<tr>
<th>What looked green</th>
<th>What was actually broken</th>
</tr>
</thead>
<tbody>
<tr>
<td>Build passed</td>
<td>The app failed at startup because of a DI registration issue</td>
</tr>
<tr>
<td>Tests passed</td>
<td>Runtime service resolution still failed</td>
</tr>
<tr>
<td>Smoke-test passed</td>
<td>Settings crashed with a XAML parse error</td>
</tr>
<tr>
<td>App launched</td>
<td>Exit logged shutdown messages but the process stayed alive</td>
</tr>
<tr>
<td>Settings opened</td>
<td>Settings was read-only</td>
</tr>
<tr>
<td>Settings became editable</td>
<td>Save froze the app</td>
</tr>
<tr>
<td>Tests were added for tray behavior</td>
<td>Tests hung for more than 25 minutes</td>
</tr>
</tbody>
</table>
<p>That table is basically the whole lesson.</p>
<p>Build green was not enough.</p>
<p>Tests green was not enough.</p>
<p>Even the first smoke-test was not enough.</p>
<p>Every time we made the definition of “working” more realistic, we found the next missing piece.</p>
<h2>The DI problem</h2>
<p>One of the first runtime issues was dependency injection.</p>
<p>The app built. Tests passed. But <code>dotnet run</code> failed because a service required <code>NetAgentOptions</code>, and the DI container did not know how to resolve it.</p>
<p>This was the first big reminder:</p>
<pre><code class="language-text">A build tells you the code compiles.
It does not tell you the app can start.
</code></pre>
<p>So the quality gate had to include a runtime startup validation.</p>
<p>That led to adding a smoke-test mode:</p>
<pre><code class="language-powershell">dotnet run --project src\ElBruno.NetAgent -- --smoke-test
</code></pre>
<p>At first, the smoke-test resolved core services and validated config.</p>
<p>Later, it had to do more.</p>
<h2>The XAML problem</h2>
<p>After fixing startup, the app launched and the tray menu opened.</p>
<p>Great, right?</p>
<p>Not yet.</p>
<p>Clicking Open Settings caused a runtime XAML parse exception.</p>
<p>The cause was a stray raw string inside a WPF UI container. Build passed. Tests passed. The first smoke-test passed. But the Settings window crashed when actually constructed by the app.</p>
<p>So the smoke-test evolved again.</p>
<p>It started validating that these windows could be constructed without being shown:</p>
<ul>
<li><code>SettingsWindow</code></li>
<li><code>StatusWindow</code></li>
<li><code>NetworkSelectorWindow</code></li>
</ul>
<p>That was another useful lesson:</p>
<pre><code class="language-text">A smoke-test that only resolves services is not enough for a desktop app.
It should catch XAML load errors too.
</code></pre>
<h2>The Exit problem</h2>
<p>Another fun one: the app would log that it was shutting down, but the process did not exit.</p>
<p>The logs looked promising:</p>
<pre><code class="language-text">Exit clicked.
Application is shutting down...
TrayIconService stopping.
</code></pre>
<p>But the PowerShell prompt did not return.</p>
<p>That meant something was still alive: a dispatcher, a tray resource, a hosted service, a foreground thread, or some combination of all the usual desktop app suspects.</p>
<p>The Exit path had to become explicit:</p>
<ul>
<li>hide/disable the tray icon</li>
<li>dispose the tray icon adapter</li>
<li>dispose context menu resources</li>
<li>close WPF windows</li>
<li>request WPF Application shutdown on the UI dispatcher</li>
<li>stop hosted services</li>
<li>make sure no foreground thread keeps the process alive</li>
</ul>
<p>Eventually, Exit worked and the app returned to PowerShell.</p>
<p>This became another gate:</p>
<pre><code class="language-powershell">dotnet run --project src\ElBruno.NetAgent -- --smoke-test-exit
</code></pre>
<h2>The test hang problem</h2>
<p>This was probably the most painful stabilization loop.</p>
<p>At one point, the test suite was still running after more than 25 minutes.</p>
<p>That is not a slow test.</p>
<p>That is a test hang.</p>
<p>The root cause was classic desktop-app testing pain: tests were touching native <code>System.Windows.Forms.NotifyIcon</code>, WPF Dispatcher behavior, and tray interactions as if they were normal unit tests.</p>
<p>They were not.</p>
<p>The fix was architectural:</p>
<ul>
<li>introduce <code>INotifyIconAdapter</code></li>
<li>use <code>TestNotifyIconAdapter</code> in unit tests</li>
<li>stop testing native NotifyIcon behavior in normal unit tests</li>
<li>mark real tray/WPF interaction tests as skipped/manual integration tests</li>
<li>use <code>disable-parallel.runsettings</code></li>
<li>keep the full unit test suite fast</li>
</ul>
<p>After that, the test suite went back to a few seconds.</p>
<p>The new rule became:</p>
<pre><code class="language-text">A quality gate is not valid unless tests finish quickly.
</code></pre>
<p>And another one:</p>
<pre><code class="language-text">If your tests need a real tray icon, they are probably not unit tests anymore.
</code></pre>
<h2>The Settings problem</h2>
<p>Once the app launched, Settings opened, and Exit worked, I found another issue.</p>
<p>Settings was read-only.</p>
<p>It showed useful information:</p>
<ul>
<li>Dry Run Mode</li>
<li>Auto Mode Enabled</li>
<li>Config File Path</li>
<li>App Data Folder</li>
</ul>
<p>But it did not allow editing the actual safe switching rules.</p>
<p>So Settings had to become a real configuration view.</p>
<p>The final Settings behavior included editable safe options like:</p>
<ul>
<li>DryRunMode</li>
<li>AutoModeEnabled</li>
<li>auto mode interval seconds</li>
<li>minimum quality score required to switch</li>
<li>minimum score improvement required</li>
<li>preferred interface names or patterns</li>
<li>excluded interface names or patterns</li>
<li>excluded interface kinds like Virtual, Loopback, VPN, Unknown</li>
<li>prefer USB tethering</li>
<li>allow Wi-Fi</li>
<li>allow Ethernet</li>
<li>ignore virtual adapters</li>
<li>ignore loopback adapters</li>
<li>ignore VPN adapters</li>
<li>ignore down or unknown-status adapters</li>
<li>Save</li>
<li>Reload</li>
<li>Reset to Safe Defaults</li>
</ul>
<p>Still no real network mutation.</p>
<p>Still dry-run first.</p>
<p>Still safe by default.</p>
<h2>The Save freeze problem</h2>
<p>Then came the final boss.</p>
<p>Settings was editable. The Save button existed. The values could be changed.</p>
<p>Then I clicked Save.</p>
<p>The app froze.</p>
<p>Build was green. Tests were green. Smoke-test was green. The Settings UI opened. Everything looked fine.</p>
<p>But a real user click exposed the bug.</p>
<p>The root cause was a classic UI-thread blocking issue.</p>
<p><code>RelayCommand.Execute</code> was synchronously waiting on async code using <code>GetAwaiter().GetResult()</code>.</p>
<p>That blocked the WPF UI thread during Save.</p>
<p>The fix was small but important:</p>
<ul>
<li>stop blocking the UI thread</li>
<li>let Save run asynchronously</li>
<li>add a smoke helper for Settings Save</li>
</ul>
<p>The new validation became:</p>
<pre><code class="language-powershell">dotnet run --project src\ElBruno.NetAgent -- --smoke-test-settings-save
</code></pre>
<p>That smoke-test verifies that Settings Save returns quickly and does not freeze.</p>
<p>This is exactly the kind of bug you only catch when you validate real app behavior, not just generated code.</p>
<h2>How the quality gate evolved</h2>
<p>At the beginning, the quality gate was simple.</p>
<pre><code class="language-powershell">dotnet build
dotnet test
</code></pre>
<p>That was not enough.</p>
<p>Then it became:</p>
<pre><code class="language-powershell">dotnet build -c Release
dotnet test -c Release --no-build
</code></pre>
<p>Still not enough.</p>
<p>Then we added runtime validation:</p>
<pre><code class="language-powershell">dotnet run --project src\ElBruno.NetAgent -- --smoke-test
</code></pre>
<p>Then we learned that <code>dotnet test --no-build</code> can lie after a failed build because it may run stale binaries.</p>
<p>So stabilization needed:</p>
<pre><code class="language-powershell">dotnet clean
dotnet build -c Release
dotnet test -c Release --no-build --settings disable-parallel.runsettings
</code></pre>
<p>Then we added targeted runtime checks:</p>
<pre><code class="language-powershell">dotnet run --project src\ElBruno.NetAgent -- --smoke-test
dotnet run --project src\ElBruno.NetAgent -- --smoke-test-exit
dotnet run --project src\ElBruno.NetAgent -- --smoke-test-settings-save
</code></pre>
<p>And finally, manual validation was mandatory:</p>
<ul>
<li>launch the app</li>
<li>open the tray menu</li>
<li>open Status</li>
<li>open Network Selector</li>
<li>open Settings</li>
<li>edit settings</li>
<li>save settings</li>
<li>confirm the UI does not freeze</li>
<li>close and reopen Settings</li>
<li>confirm values persisted</li>
<li>reset to safe defaults</li>
<li>preview best switch in dry-run mode</li>
<li>open Help/About</li>
<li>confirm the repo URL is shown</li>
<li>Exit</li>
<li>confirm PowerShell prompt returns</li>
</ul>
<p>This is the real quality gate.</p>
<p>Not because I like long checklists.</p>
<p>Because every missing item in that list caught a real bug.</p>
<h2>The cost</h2>
<p>Microsoft Foundry Monitor showed the following final-ish snapshot for this GPT-5-mini BYOK experiment:</p>
<ul>
<li>Total requests: 4.16K</li>
<li>Total token count: 104.27M</li>
<li>Input tokens: 103.05M</li>
<li>Output tokens: 1.22M</li>
<li>Estimated total cost: $6.81</li>
</ul>
<p>The dashboard estimate changed a little between refreshes, so I am treating this as a monitor estimate, not a final billing invoice.</p>
<p>But the order of magnitude is clear.</p>
<p>For roughly 104M tokens and about $6–$7 estimated cost, GPT-5-mini + SQUAD generated and stabilized a working Windows tray app.</p>
<p>That is kind of wild.</p>
<p>But here is the important part:</p>
<p>The Azure bill was not the blocker.</p>
<p>The hard part was teaching the agents what “working software” actually means.</p>
<h2>What worked well</h2>
<p>The biggest win was that GPT-5-mini made SQUAD practical again.</p>
<p>With the local models, the workflow needed very constrained prompts and constant babysitting. With GPT-5-mini BYOK, background agents could run, split work, report back, commit changes, and recover from some failures.</p>
<p>Other wins:</p>
<ul>
<li>scaffolding moved quickly</li>
<li>DI issues were fixable with targeted prompts</li>
<li>tests could be added and improved incrementally</li>
<li>docs and CI were updated along the way</li>
<li>the model handled refactors like <code>INotifyIconAdapter</code></li>
<li>smoke-test modes became part of the app</li>
<li>the app reached a real working state</li>
</ul>
<p>This was not perfect autonomy.</p>
<p>But it was much closer to a real agent workflow than the local model runs.</p>
<h2>What did not work well</h2>
<p>The model still needed strong human checkpoints.</p>
<p>Some examples:</p>
<ul>
<li>It trusted build/test success too much.</li>
<li>It sometimes committed before the final validation was really clean.</li>
<li>It created tests that were technically reasonable but bad for desktop app stability.</li>
<li>It mixed feature work and stabilization work when the prompt was too broad.</li>
<li>It needed repeated reminders to stop feature expansion and fix one bug only.</li>
<li>It sometimes solved the current failure but missed the product behavior.</li>
</ul>
<p>The biggest pattern was this:</p>
<p>When the task was too broad, the agents drifted.</p>
<p>When the task was narrow, they were useful.</p>
<p>That is a big lesson for the GPT-5.5 experiment.</p>
<h2>The lessons learned</h2>
<p>Here are the biggest lessons I am taking from this run.</p>
<h3>1. SQUAD became practical with cloud BYOK</h3>
<p>This was the biggest difference compared with local models.</p>
<p>Local CPU was possible but painful.</p>
<p>Local GPU was much better but still fragile.</p>
<p>GPT-5-mini BYOK made agent orchestration feel realistic.</p>
<h3>2. Build green is not enough</h3>
<p>The app compiled before it worked.</p>
<p>A lot.</p>
<h3>3. Tests green is not enough</h3>
<p>Tests can miss runtime DI, XAML, tray lifecycle, and real UI behavior.</p>
<p>Also, <code>dotnet test --no-build</code> can give false confidence after a failed build.</p>
<h3>4. Smoke-tests must evolve</h3>
<p>The smoke-test started as service resolution.</p>
<p>Then it had to include UI construction.</p>
<p>Then it needed exit validation.</p>
<p>Then Settings Save validation.</p>
<p>Every new smoke-test came from a real bug.</p>
<h3>5. Desktop apps need manual UX validation</h3>
<p>This is not optional.</p>
<p>A tray app can pass build, tests, and smoke-test, and still fail when a human clicks Save.</p>
<h3>6. Native tray tests are not unit tests</h3>
<p>Testing <code>NotifyIcon</code>, WPF Dispatcher, and real tray behavior in normal unit tests caused long hangs.</p>
<p>Adapters and fakes are not “nice to have.”</p>
<p>They are required.</p>
<h3>7. The stabilization phase matters</h3>
<p>The app generation phase was impressive.</p>
<p>The stabilization phase was where the real engineering happened.</p>
<h3>8. The code was cheap. The decisions were expensive.</h3>
<p>This is the core lesson.</p>
<p>The model can generate code quickly.</p>
<p>But deciding what to validate, where to put guardrails, what tests count, what safety means, and when the app is actually working — that is where the value is.</p>
<h2>What this means for GPT-5.5</h2>
<p>The next test will use GPT-5.5.</p>
<p>But the goal is not simply:</p>
<pre><code class="language-text">Can GPT-5.5 write more code?
</code></pre>
<p>That is not interesting enough.</p>
<p>The real question is:</p>
<pre><code class="language-text">Can GPT-5.5 reduce the stabilization loops?
</code></pre>
<p>For the GPT-5.5 experiment, I will start with all the lessons from this run baked in from minute zero:</p>
<ul>
<li>normalize SQUAD model routing first</li>
<li>define product behavior first</li>
<li>define safety guardrails first</li>
<li>define quality gates first</li>
<li>require <code>--smoke-test</code></li>
<li>require <code>--smoke-test-exit</code></li>
<li>require <code>--smoke-test-settings-save</code></li>
<li>use <code>disable-parallel.runsettings</code></li>
<li>avoid native NotifyIcon/WPF Dispatcher in unit tests</li>
<li>keep tray/WPF tests as manual integration tests</li>
<li>require manual UX validation before calling the app done</li>
</ul>
<p>That should make the comparison much more interesting.</p>
<p>If GPT-5.5 is stronger, I do not only want to see better code.</p>
<p>I want to see fewer rescue loops.</p>
<p>Fewer runtime DI issues.</p>
<p>Fewer XAML mistakes.</p>
<p>Fewer test architecture problems.</p>
<p>Fewer human corrections.</p>
<p>That is the real test.</p>
</div>



<div class="wp-block-jetpack-markdown"><h2>Final thoughts</h2>
<p>This experiment convinced me of something important.</p>
<p>AI can generate a lot of code very quickly.</p>
<p>But working software is not the same as generated code.</p>
<p>Working software means startup validation, DI checks, XAML loading, fast deterministic tests, safe defaults, manual UX validation, and boring things like “Save does not freeze the app.”</p>
<p>That boring stuff matters.</p>
<p>Actually, that boring stuff is the work.</p>
<p>So yes, GPT-5-mini + SQUAD built the app.</p>
<p>But the quality gates made it usable.</p>
<p>The code was cheap.</p>
<p>The decisions were expensive.</p>
<p>Next stop: same experiment, same repo, same rules — but with GPT-5.5.</p>
</div>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/05/11/github-copilot-cli-gpt-5-mini-byok-the-code-was-cheap-the-quality-gates-were-expensive/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40317</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_vpj2hyvpj2hyvpj2-small.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_vpj2hyvpj2hyvpj2-small.png">
			<media:title type="html">Gemini_Generated_Image_vpj2hyvpj2hyvpj2-small</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_vpj2hyvpj2hyvpj2-small.png?w=688"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/image-1.png?w=756"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/image-2.png?w=759"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/image-3.png?w=711"/>
	</item>
		<item>
		<title>From Pet Projects to +20 Open Source NuGet Packages, Thanks to GitHub Copilot &#128640;</title>
		<link>https://elbruno.com/2026/05/10/from-pet-projects-to-20-open-source-nuget-packages-thanks-to-github-copilot-%f0%9f%9a%80/</link>
					<comments>https://elbruno.com/2026/05/10/from-pet-projects-to-20-open-source-nuget-packages-thanks-to-github-copilot-%f0%9f%9a%80/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Sun, 10 May 2026 12:10:31 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[GitHub]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[NuGet]]></category>
		<category><![CDATA[NuGet Packages]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40311</guid>

					<description><![CDATA[Hi! Some of these started as small pet projects. IE: a quick helper for a demo, a tiny tool for a conference, a library to avoid repeating the same code again and again, or one of those “I’ll just build this in one evening” ideas that somehow becomes a real thing. And now, thanks to [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" width="836" height="471" src="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-10-2026-08_03_42-am-small.png?w=836" alt="" class="wp-image-40313" srcset="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-10-2026-08_03_42-am-small.png 836w, https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-10-2026-08_03_42-am-small.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-10-2026-08_03_42-am-small.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-10-2026-08_03_42-am-small.png?w=768 768w" sizes="auto, (max-width: 836px) 100vw, 836px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">Some of these started as small pet projects.</p>



<p class="wp-block-paragraph">IE: a quick helper for a demo, a tiny tool for a conference, a library to avoid repeating the same code again and again, or one of those “I’ll just build this in one evening” ideas that somehow becomes a real thing.</p>



<p class="wp-block-paragraph">And now, thanks to GitHub Copilot, many of these experiments are becoming open source, free NuGet packages that I hope are useful to everyone.</p>



<p class="wp-block-paragraph">Some are focused on AI. Some are focused on local models. Some help with embeddings, speech, QR codes, document processing, MCP tools, agents, and developer productivity.</p>



<p class="wp-block-paragraph">In other words: a beautiful collection of useful chaos.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Quick package index</h2>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Versions are current as of the latest dashboard snapshot: 2026-05-10.<br />Dashboard repo: <a href="https://github.com/elbruno/nuget-repo-dashboard?utm_source=chatgpt.com">https://github.com/elbruno/nuget-repo-dashboard</a></p>
</blockquote>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Package</th><th>Version</th><th>NuGet</th><th>GitHub</th></tr></thead><tbody><tr><td>ElBruno.AI.Evaluation</td><td>0.5.2</td><td><a><a href="https://www.nuget.org/packages/ElBruno.AI.Evaluation" rel="nofollow">https://www.nuget.org/packages/ElBruno.AI.Evaluation</a></a></td><td><a><a href="https://github.com/elbruno/elbruno-ai-evaluation" rel="nofollow">https://github.com/elbruno/elbruno-ai-evaluation</a></a></td></tr><tr><td>ElBruno.AI.Evaluation.Reporting</td><td>0.5.2</td><td><a><a href="https://www.nuget.org/packages/ElBruno.AI.Evaluation.Reporting" rel="nofollow">https://www.nuget.org/packages/ElBruno.AI.Evaluation.Reporting</a></a></td><td><a><a href="https://github.com/elbruno/elbruno-ai-evaluation" rel="nofollow">https://github.com/elbruno/elbruno-ai-evaluation</a></a></td></tr><tr><td>ElBruno.AI.Evaluation.SyntheticData</td><td>0.5.2</td><td><a><a href="https://www.nuget.org/packages/ElBruno.AI.Evaluation.SyntheticData" rel="nofollow">https://www.nuget.org/packages/ElBruno.AI.Evaluation.SyntheticData</a></a></td><td><a><a href="https://github.com/elbruno/elbruno-ai-evaluation" rel="nofollow">https://github.com/elbruno/elbruno-ai-evaluation</a></a></td></tr><tr><td>ElBruno.AI.Evaluation.Xunit</td><td>0.5.2</td><td><a><a href="https://www.nuget.org/packages/ElBruno.AI.Evaluation.Xunit" rel="nofollow">https://www.nuget.org/packages/ElBruno.AI.Evaluation.Xunit</a></a></td><td><a><a href="https://github.com/elbruno/elbruno-ai-evaluation" rel="nofollow">https://github.com/elbruno/elbruno-ai-evaluation</a></a></td></tr><tr><td>ElBruno.AgentsOrchestration.Abstractions</td><td>0.5.5</td><td><a href="https://www.nuget.org/packages/ElBruno.AgentsOrchestration.Abstractions">https://www.nuget.org/packages/ElBruno.AgentsOrchestration.Abstractions</a></td><td><a href="https://github.com/elbruno/elbruno.agentsorchestration?utm_source=chatgpt.com">https://github.com/elbruno/elbruno.agentsorchestration</a></td></tr><tr><td>ElBruno.AgentsOrchestration.Orchestration</td><td>0.5.5</td><td><a href="https://www.nuget.org/packages/ElBruno.AgentsOrchestration.Orchestration">https://www.nuget.org/packages/ElBruno.AgentsOrchestration.Orchestration</a></td><td><a href="https://github.com/elbruno/elbruno.agentsorchestration?utm_source=chatgpt.com">https://github.com/elbruno/elbruno.agentsorchestration</a></td></tr><tr><td>ElBruno.AotMapper</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.AotMapper" rel="nofollow">https://www.nuget.org/packages/ElBruno.AotMapper</a></a></td><td><a href="https://github.com/elbruno/ElBruno.AotMapper?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.AotMapper</a></td></tr><tr><td>ElBruno.AotMapper.AspNetCore</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.AotMapper.AspNetCore" rel="nofollow">https://www.nuget.org/packages/ElBruno.AotMapper.AspNetCore</a></a></td><td><a href="https://github.com/elbruno/ElBruno.AotMapper?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.AotMapper</a></td></tr><tr><td>ElBruno.AotMapper.EntityFramework</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.AotMapper.EntityFramework" rel="nofollow">https://www.nuget.org/packages/ElBruno.AotMapper.EntityFramework</a></a></td><td><a href="https://github.com/elbruno/ElBruno.AotMapper?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.AotMapper</a></td></tr><tr><td>ElBruno.AotMapper.Generator</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.AotMapper.Generator" rel="nofollow">https://www.nuget.org/packages/ElBruno.AotMapper.Generator</a></a></td><td><a href="https://github.com/elbruno/ElBruno.AotMapper?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.AotMapper</a></td></tr><tr><td>ElBruno.AspireMonitor</td><td>1.5.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.AspireMonitor" rel="nofollow">https://www.nuget.org/packages/ElBruno.AspireMonitor</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.AspireMonitor" rel="nofollow">https://github.com/elbruno/ElBruno.AspireMonitor</a></a></td></tr><tr><td>ElBruno.BM25</td><td>0.5.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.BM25" rel="nofollow">https://www.nuget.org/packages/ElBruno.BM25</a></a></td><td><a><a href="https://github.com/ElBruno/ElBruno.BM25" rel="nofollow">https://github.com/ElBruno/ElBruno.BM25</a></a></td></tr><tr><td>ElBruno.ClockTray</td><td>1.0.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.ClockTray" rel="nofollow">https://www.nuget.org/packages/ElBruno.ClockTray</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.ClockTray" rel="nofollow">https://github.com/elbruno/ElBruno.ClockTray</a></a></td></tr><tr><td>elbruno.Extensions.AI.Skills.Core</td><td>—</td><td><a><a href="https://www.nuget.org/packages/elbruno.Extensions.AI.Skills.Core" rel="nofollow">https://www.nuget.org/packages/elbruno.Extensions.AI.Skills.Core</a></a></td><td><a><a href="https://github.com/elbruno/elbruno-agentskills" rel="nofollow">https://github.com/elbruno/elbruno-agentskills</a></a></td></tr><tr><td>elbruno.Extensions.AI.Skills.Cli</td><td>—</td><td><a><a href="https://www.nuget.org/packages/elbruno.Extensions.AI.Skills.Cli" rel="nofollow">https://www.nuget.org/packages/elbruno.Extensions.AI.Skills.Cli</a></a></td><td><a><a href="https://github.com/elbruno/elbruno-agentskills" rel="nofollow">https://github.com/elbruno/elbruno-agentskills</a></a></td></tr><tr><td>ElBruno.HuggingFace.Downloader</td><td>1.1.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.HuggingFace.Downloader" rel="nofollow">https://www.nuget.org/packages/ElBruno.HuggingFace.Downloader</a></a></td><td><a href="https://github.com/elbruno/ElBruno.HuggingFace.Downloader">https://github.com/elbruno/ElBruno.HuggingFace.Downloader</a></td></tr><tr><td>ElBruno.HuggingFace.Downloader.Cli</td><td>—</td><td><a><a href="https://www.nuget.org/packages/ElBruno.HuggingFace.Downloader.Cli" rel="nofollow">https://www.nuget.org/packages/ElBruno.HuggingFace.Downloader.Cli</a></a></td><td><a href="https://github.com/elbruno/ElBruno.HuggingFace.Downloader">https://github.com/elbruno/ElBruno.HuggingFace.Downloader</a></td></tr><tr><td>ElBruno.LocalEmbeddings</td><td>1.4.6</td><td><a><a href="https://www.nuget.org/packages/ElBruno.LocalEmbeddings" rel="nofollow">https://www.nuget.org/packages/ElBruno.LocalEmbeddings</a></a></td><td><a href="https://github.com/elbruno/elbruno.localembeddings">https://github.com/elbruno/elbruno.localembeddings</a></td></tr><tr><td>ElBruno.LocalEmbeddings.Harrier</td><td>1.4.6</td><td><a><a href="https://www.nuget.org/packages/ElBruno.LocalEmbeddings.Harrier" rel="nofollow">https://www.nuget.org/packages/ElBruno.LocalEmbeddings.Harrier</a></a></td><td><a href="https://github.com/elbruno/elbruno.localembeddings">https://github.com/elbruno/elbruno.localembeddings</a></td></tr><tr><td>ElBruno.LocalEmbeddings.ImageEmbeddings</td><td>1.4.6</td><td><a><a href="https://www.nuget.org/packages/ElBruno.LocalEmbeddings.ImageEmbeddings" rel="nofollow">https://www.nuget.org/packages/ElBruno.LocalEmbeddings.ImageEmbeddings</a></a></td><td><a href="https://github.com/elbruno/elbruno.localembeddings">https://github.com/elbruno/elbruno.localembeddings</a></td></tr><tr><td>ElBruno.LocalEmbeddings.ImageEmbeddings.Downloader</td><td>1.4.6</td><td><a><a href="https://www.nuget.org/packages/ElBruno.LocalEmbeddings.ImageEmbeddings.Downloader" rel="nofollow">https://www.nuget.org/packages/ElBruno.LocalEmbeddings.ImageEmbeddings.Downloader</a></a></td><td><a href="https://github.com/elbruno/elbruno.localembeddings">https://github.com/elbruno/elbruno.localembeddings</a></td></tr><tr><td>ElBruno.LocalEmbeddings.KernelMemory</td><td>1.4.6</td><td><a href="https://www.nuget.org/packages/ElBruno.LocalEmbeddings.KernelMemory">https://www.nuget.org/packages/ElBruno.LocalEmbeddings.KernelMemory</a></td><td><a href="https://github.com/elbruno/elbruno.localembeddings">https://github.com/elbruno/elbruno.localembeddings</a></td></tr><tr><td>ElBruno.LocalEmbeddings.Npu</td><td>1.4.6</td><td><a><a href="https://www.nuget.org/packages/ElBruno.LocalEmbeddings.Npu" rel="nofollow">https://www.nuget.org/packages/ElBruno.LocalEmbeddings.Npu</a></a></td><td><a href="https://github.com/elbruno/elbruno.localembeddings">https://github.com/elbruno/elbruno.localembeddings</a></td></tr><tr><td>ElBruno.LocalEmbeddings.Npu.Intel</td><td>1.4.6</td><td><a><a href="https://www.nuget.org/packages/ElBruno.LocalEmbeddings.Npu.Intel" rel="nofollow">https://www.nuget.org/packages/ElBruno.LocalEmbeddings.Npu.Intel</a></a></td><td><a href="https://github.com/elbruno/elbruno.localembeddings">https://github.com/elbruno/elbruno.localembeddings</a></td></tr><tr><td>ElBruno.LocalEmbeddings.Npu.Qualcomm</td><td>1.4.6</td><td><a><a href="https://www.nuget.org/packages/ElBruno.LocalEmbeddings.Npu.Qualcomm" rel="nofollow">https://www.nuget.org/packages/ElBruno.LocalEmbeddings.Npu.Qualcomm</a></a></td><td><a href="https://github.com/elbruno/elbruno.localembeddings">https://github.com/elbruno/elbruno.localembeddings</a></td></tr><tr><td>ElBruno.LocalEmbeddings.VectorData</td><td>1.4.6</td><td><a href="https://www.nuget.org/packages/ElBruno.LocalEmbeddings.VectorData">https://www.nuget.org/packages/ElBruno.LocalEmbeddings.VectorData</a></td><td><a href="https://github.com/elbruno/elbruno.localembeddings">https://github.com/elbruno/elbruno.localembeddings</a></td></tr><tr><td>ElBruno.LocalLLMs</td><td>0.16.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.LocalLLMs" rel="nofollow">https://www.nuget.org/packages/ElBruno.LocalLLMs</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.LocalLLMs" rel="nofollow">https://github.com/elbruno/ElBruno.LocalLLMs</a></a></td></tr><tr><td>ElBruno.LocalLLMs.BitNet</td><td>0.16.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.LocalLLMs.BitNet" rel="nofollow">https://www.nuget.org/packages/ElBruno.LocalLLMs.BitNet</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.LocalLLMs" rel="nofollow">https://github.com/elbruno/ElBruno.LocalLLMs</a></a></td></tr><tr><td>ElBruno.LocalLLMs.Rag</td><td>0.16.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.LocalLLMs.Rag" rel="nofollow">https://www.nuget.org/packages/ElBruno.LocalLLMs.Rag</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.LocalLLMs" rel="nofollow">https://github.com/elbruno/ElBruno.LocalLLMs</a></a></td></tr><tr><td>ElBruno.MarkItDotNet</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet" rel="nofollow">https://www.nuget.org/packages/ElBruno.MarkItDotNet</a></a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.MarkItDotNet.AI</td><td>0.6.0</td><td><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.AI">https://www.nuget.org/packages/ElBruno.MarkItDotNet.AI</a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.MarkItDotNet.AzureSearch</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.AzureSearch" rel="nofollow">https://www.nuget.org/packages/ElBruno.MarkItDotNet.AzureSearch</a></a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.MarkItDotNet.Chunking</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.Chunking" rel="nofollow">https://www.nuget.org/packages/ElBruno.MarkItDotNet.Chunking</a></a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.MarkItDotNet.Citations</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.Citations" rel="nofollow">https://www.nuget.org/packages/ElBruno.MarkItDotNet.Citations</a></a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.MarkItDotNet.Cli</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.Cli" rel="nofollow">https://www.nuget.org/packages/ElBruno.MarkItDotNet.Cli</a></a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.MarkItDotNet.CoreModel</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.CoreModel" rel="nofollow">https://www.nuget.org/packages/ElBruno.MarkItDotNet.CoreModel</a></a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.MarkItDotNet.DocumentIntelligence</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.DocumentIntelligence" rel="nofollow">https://www.nuget.org/packages/ElBruno.MarkItDotNet.DocumentIntelligence</a></a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.MarkItDotNet.Excel</td><td>0.6.0</td><td><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.Excel">https://www.nuget.org/packages/ElBruno.MarkItDotNet.Excel</a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.MarkItDotNet.Metadata</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.Metadata" rel="nofollow">https://www.nuget.org/packages/ElBruno.MarkItDotNet.Metadata</a></a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.MarkItDotNet.PowerPoint</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.PowerPoint" rel="nofollow">https://www.nuget.org/packages/ElBruno.MarkItDotNet.PowerPoint</a></a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.MarkItDotNet.Quality</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.Quality" rel="nofollow">https://www.nuget.org/packages/ElBruno.MarkItDotNet.Quality</a></a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.MarkItDotNet.Sync</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.Sync" rel="nofollow">https://www.nuget.org/packages/ElBruno.MarkItDotNet.Sync</a></a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.MarkItDotNet.VectorData</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.VectorData" rel="nofollow">https://www.nuget.org/packages/ElBruno.MarkItDotNet.VectorData</a></a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.MarkItDotNet.Whisper</td><td>0.6.0</td><td><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet.Whisper">https://www.nuget.org/packages/ElBruno.MarkItDotNet.Whisper</a></td><td><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></td></tr><tr><td>ElBruno.ModelContextProtocol.MCPToolRouter</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.ModelContextProtocol.MCPToolRouter" rel="nofollow">https://www.nuget.org/packages/ElBruno.ModelContextProtocol.MCPToolRouter</a></a></td><td><a href="https://github.com/elbruno/ElBruno.ModelContextProtocol">https://github.com/elbruno/ElBruno.ModelContextProtocol</a></td></tr><tr><td>ElBruno.OllamaMonitor</td><td>0.7.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.OllamaMonitor" rel="nofollow">https://www.nuget.org/packages/ElBruno.OllamaMonitor</a></a></td><td><a href="https://github.com/elbruno/ElBruno.OllamaMonitor?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.OllamaMonitor</a></td></tr><tr><td>ElBruno.OllamaSharp.Extensions</td><td>1.0.2</td><td><a><a href="https://www.nuget.org/packages/ElBruno.OllamaSharp.Extensions" rel="nofollow">https://www.nuget.org/packages/ElBruno.OllamaSharp.Extensions</a></a></td><td><a href="https://github.com/elbruno/elbruno.OllamaSharp.Extensions">https://github.com/elbruno/elbruno.OllamaSharp.Extensions</a></td></tr><tr><td>ElBruno.PersonaPlex</td><td>0.6.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.PersonaPlex" rel="nofollow">https://www.nuget.org/packages/ElBruno.PersonaPlex</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.PersonaPlex" rel="nofollow">https://github.com/elbruno/ElBruno.PersonaPlex</a></a></td></tr><tr><td>ElBruno.QRCodeGenerator.Ascii</td><td>1.1.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.QRCodeGenerator.Ascii" rel="nofollow">https://www.nuget.org/packages/ElBruno.QRCodeGenerator.Ascii</a></a></td><td><a href="https://github.com/elbruno/ElBruno.QRCodeGenerator?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.QRCodeGenerator</a></td></tr><tr><td>ElBruno.QRCodeGenerator.CLI</td><td>1.1.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.QRCodeGenerator.CLI" rel="nofollow">https://www.nuget.org/packages/ElBruno.QRCodeGenerator.CLI</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.QRCodeGenerator.CLI" rel="nofollow">https://github.com/elbruno/ElBruno.QRCodeGenerator.CLI</a></a></td></tr><tr><td>ElBruno.QRCodeGenerator.Image</td><td>1.1.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.QRCodeGenerator.Image" rel="nofollow">https://www.nuget.org/packages/ElBruno.QRCodeGenerator.Image</a></a></td><td><a href="https://github.com/elbruno/ElBruno.QRCodeGenerator?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.QRCodeGenerator</a></td></tr><tr><td>ElBruno.QRCodeGenerator.Payloads</td><td>1.1.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.QRCodeGenerator.Payloads" rel="nofollow">https://www.nuget.org/packages/ElBruno.QRCodeGenerator.Payloads</a></a></td><td><a href="https://github.com/elbruno/ElBruno.QRCodeGenerator?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.QRCodeGenerator</a></td></tr><tr><td>ElBruno.QRCodeGenerator.Pdf</td><td>1.1.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.QRCodeGenerator.Pdf" rel="nofollow">https://www.nuget.org/packages/ElBruno.QRCodeGenerator.Pdf</a></a></td><td><a href="https://github.com/elbruno/ElBruno.QRCodeGenerator?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.QRCodeGenerator</a></td></tr><tr><td>ElBruno.QRCodeGenerator.Svg</td><td>1.1.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.QRCodeGenerator.Svg" rel="nofollow">https://www.nuget.org/packages/ElBruno.QRCodeGenerator.Svg</a></a></td><td><a href="https://github.com/elbruno/ElBruno.QRCodeGenerator?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.QRCodeGenerator</a></td></tr><tr><td>ElBruno.QRCodeGenerator.Tool</td><td>1.1.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.QRCodeGenerator.Tool" rel="nofollow">https://www.nuget.org/packages/ElBruno.QRCodeGenerator.Tool</a></a></td><td><a href="https://github.com/elbruno/ElBruno.QRCodeGenerator?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.QRCodeGenerator</a></td></tr><tr><td>ElBruno.QwenTTS</td><td>1.4.7</td><td><a><a href="https://www.nuget.org/packages/ElBruno.QwenTTS" rel="nofollow">https://www.nuget.org/packages/ElBruno.QwenTTS</a></a></td><td><a href="https://github.com/elbruno/ElBruno.QwenTTS">https://github.com/elbruno/ElBruno.QwenTTS</a></td></tr><tr><td>ElBruno.QwenTTS.VoiceCloning</td><td>1.4.7</td><td><a><a href="https://www.nuget.org/packages/ElBruno.QwenTTS.VoiceCloning" rel="nofollow">https://www.nuget.org/packages/ElBruno.QwenTTS.VoiceCloning</a></a></td><td><a href="https://github.com/elbruno/ElBruno.QwenTTS">https://github.com/elbruno/ElBruno.QwenTTS</a></td></tr><tr><td>ElBruno.Realtime</td><td>0.10.0</td><td><a href="https://www.nuget.org/packages/ElBruno.Realtime">https://www.nuget.org/packages/ElBruno.Realtime</a></td><td><a href="https://github.com/elbruno/ElBruno.Realtime?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.Realtime</a></td></tr><tr><td>ElBruno.Realtime.SileroVad</td><td>0.10.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.Realtime.SileroVad" rel="nofollow">https://www.nuget.org/packages/ElBruno.Realtime.SileroVad</a></a></td><td><a href="https://github.com/elbruno/ElBruno.Realtime?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.Realtime</a></td></tr><tr><td>ElBruno.Realtime.Whisper</td><td>0.10.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.Realtime.Whisper" rel="nofollow">https://www.nuget.org/packages/ElBruno.Realtime.Whisper</a></a></td><td><a href="https://github.com/elbruno/ElBruno.Realtime?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.Realtime</a></td></tr><tr><td>ElBruno.Reranking</td><td>0.5.1</td><td><a><a href="https://www.nuget.org/packages/ElBruno.Reranking" rel="nofollow">https://www.nuget.org/packages/ElBruno.Reranking</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.Reranking" rel="nofollow">https://github.com/elbruno/ElBruno.Reranking</a></a></td></tr><tr><td>ElBruno.Text2Image</td><td>1.2.11</td><td><a><a href="https://www.nuget.org/packages/ElBruno.Text2Image" rel="nofollow">https://www.nuget.org/packages/ElBruno.Text2Image</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.Text2Image" rel="nofollow">https://github.com/elbruno/ElBruno.Text2Image</a></a></td></tr><tr><td>ElBruno.Text2Image.Cli</td><td>1.2.11</td><td><a><a href="https://www.nuget.org/packages/ElBruno.Text2Image.Cli" rel="nofollow">https://www.nuget.org/packages/ElBruno.Text2Image.Cli</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.Text2Image" rel="nofollow">https://github.com/elbruno/ElBruno.Text2Image</a></a></td></tr><tr><td>ElBruno.Text2Image.Cpu</td><td>1.2.11</td><td><a href="https://www.nuget.org/packages/ElBruno.Text2Image.Cpu">https://www.nuget.org/packages/ElBruno.Text2Image.Cpu</a></td><td><a><a href="https://github.com/elbruno/ElBruno.Text2Image" rel="nofollow">https://github.com/elbruno/ElBruno.Text2Image</a></a></td></tr><tr><td>ElBruno.Text2Image.Cuda</td><td>1.2.11</td><td><a href="https://www.nuget.org/packages/ElBruno.Text2Image.Cuda">https://www.nuget.org/packages/ElBruno.Text2Image.Cuda</a></td><td><a><a href="https://github.com/elbruno/ElBruno.Text2Image" rel="nofollow">https://github.com/elbruno/ElBruno.Text2Image</a></a></td></tr><tr><td>ElBruno.Text2Image.DirectML</td><td>1.2.11</td><td><a href="https://www.nuget.org/packages/ElBruno.Text2Image.DirectML">https://www.nuget.org/packages/ElBruno.Text2Image.DirectML</a></td><td><a><a href="https://github.com/elbruno/ElBruno.Text2Image" rel="nofollow">https://github.com/elbruno/ElBruno.Text2Image</a></a></td></tr><tr><td>ElBruno.Text2Image.Foundry</td><td>1.2.11</td><td><a href="https://www.nuget.org/packages/ElBruno.Text2Image.Foundry">https://www.nuget.org/packages/ElBruno.Text2Image.Foundry</a></td><td><a><a href="https://github.com/elbruno/ElBruno.Text2Image" rel="nofollow">https://github.com/elbruno/ElBruno.Text2Image</a></a></td></tr><tr><td>ElBruno.VibeVoiceTTS</td><td>0.5.0</td><td><a><a href="https://www.nuget.org/packages/ElBruno.VibeVoiceTTS" rel="nofollow">https://www.nuget.org/packages/ElBruno.VibeVoiceTTS</a></a></td><td><a href="https://github.com/elbruno/ElBruno.VibeVoiceTTS">https://github.com/elbruno/ElBruno.VibeVoiceTTS</a></td></tr><tr><td>ElBruno.Whisper</td><td>0.2.0</td><td><a href="https://www.nuget.org/packages/ElBruno.Whisper">https://www.nuget.org/packages/ElBruno.Whisper</a></td><td><a><a href="https://github.com/elbruno/ElBruno.Whisper" rel="nofollow">https://github.com/elbruno/ElBruno.Whisper</a></a></td></tr><tr><td>graphify-dotnet</td><td>0.6.1</td><td><a><a href="https://www.nuget.org/packages/graphify-dotnet" rel="nofollow">https://www.nuget.org/packages/graphify-dotnet</a></a></td><td><a href="https://github.com/elbruno/graphify-dotnet">https://github.com/elbruno/graphify-dotnet</a></td></tr><tr><td>MemPalace.Core</td><td>0.15.2</td><td><a><a href="https://www.nuget.org/packages/MemPalace.Core" rel="nofollow">https://www.nuget.org/packages/MemPalace.Core</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.MempalaceNet" rel="nofollow">https://github.com/elbruno/ElBruno.MempalaceNet</a></a></td></tr><tr><td>MemPalace.Ai</td><td>0.15.2</td><td><a><a href="https://www.nuget.org/packages/MemPalace.Ai" rel="nofollow">https://www.nuget.org/packages/MemPalace.Ai</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.MempalaceNet" rel="nofollow">https://github.com/elbruno/ElBruno.MempalaceNet</a></a></td></tr><tr><td>MemPalace.Agents</td><td>0.15.2</td><td><a><a href="https://www.nuget.org/packages/MemPalace.Agents" rel="nofollow">https://www.nuget.org/packages/MemPalace.Agents</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.MempalaceNet" rel="nofollow">https://github.com/elbruno/ElBruno.MempalaceNet</a></a></td></tr><tr><td>MemPalace.Backends.Sqlite</td><td>0.15.2</td><td><a><a href="https://www.nuget.org/packages/MemPalace.Backends.Sqlite" rel="nofollow">https://www.nuget.org/packages/MemPalace.Backends.Sqlite</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.MempalaceNet" rel="nofollow">https://github.com/elbruno/ElBruno.MempalaceNet</a></a></td></tr><tr><td>MemPalace.KnowledgeGraph</td><td>0.15.2</td><td><a><a href="https://www.nuget.org/packages/MemPalace.KnowledgeGraph" rel="nofollow">https://www.nuget.org/packages/MemPalace.KnowledgeGraph</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.MempalaceNet" rel="nofollow">https://github.com/elbruno/ElBruno.MempalaceNet</a></a></td></tr><tr><td>MemPalace.Mcp</td><td>0.15.2</td><td><a><a href="https://www.nuget.org/packages/MemPalace.Mcp" rel="nofollow">https://www.nuget.org/packages/MemPalace.Mcp</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.MempalaceNet" rel="nofollow">https://github.com/elbruno/ElBruno.MempalaceNet</a></a></td></tr><tr><td>MemPalace.Mining</td><td>0.15.2</td><td><a><a href="https://www.nuget.org/packages/MemPalace.Mining" rel="nofollow">https://www.nuget.org/packages/MemPalace.Mining</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.MempalaceNet" rel="nofollow">https://github.com/elbruno/ElBruno.MempalaceNet</a></a></td></tr><tr><td>MemPalace.Search</td><td>0.15.2</td><td><a><a href="https://www.nuget.org/packages/MemPalace.Search" rel="nofollow">https://www.nuget.org/packages/MemPalace.Search</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.MempalaceNet" rel="nofollow">https://github.com/elbruno/ElBruno.MempalaceNet</a></a></td></tr><tr><td>mempalacenet</td><td>0.15.2</td><td><a><a href="https://www.nuget.org/packages/mempalacenet" rel="nofollow">https://www.nuget.org/packages/mempalacenet</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.MempalaceNet" rel="nofollow">https://github.com/elbruno/ElBruno.MempalaceNet</a></a></td></tr><tr><td>mempalacenet-bench</td><td>0.15.2</td><td><a><a href="https://www.nuget.org/packages/mempalacenet-bench" rel="nofollow">https://www.nuget.org/packages/mempalacenet-bench</a></a></td><td><a><a href="https://github.com/elbruno/ElBruno.MempalaceNet" rel="nofollow">https://github.com/elbruno/ElBruno.MempalaceNet</a></a></td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">AI evaluation: because demos are easy, validation is the hard part</h2>



<p class="wp-block-paragraph">The <code>ElBruno.AI.Evaluation</code> family is about bringing testing discipline to AI apps.</p>



<p class="wp-block-paragraph">Packages:</p>



<ul class="wp-block-list">
<li><code>ElBruno.AI.Evaluation</code></li>



<li><code>ElBruno.AI.Evaluation.Reporting</code></li>



<li><code>ElBruno.AI.Evaluation.SyntheticData</code></li>



<li><code>ElBruno.AI.Evaluation.Xunit</code></li>
</ul>



<p class="wp-block-paragraph">Repo:</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/elbruno-ai-evaluation">https://github.com/elbruno/elbruno-ai-evaluation</a></p>



<p class="wp-block-paragraph">These packages help with evaluation workflows, reporting, synthetic test data, and xUnit integration. The idea is simple: AI applications should not rely only on “it worked once in my demo.”</p>



<p class="wp-block-paragraph">They need repeatable checks.</p>



<p class="wp-block-paragraph">They need quality gates.</p>



<p class="wp-block-paragraph">They need tests that can run again tomorrow, when the model, prompt, data, or weather in the cloud changes.</p>



<p class="wp-block-paragraph">Because yes, AI apps are fun. But “trust me bro, the prompt is good” is not a testing strategy.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Agents and orchestration</h2>



<p class="wp-block-paragraph">The agents orchestration packages are focused on coordinating multiple AI agents through a lightweight workflow.</p>



<p class="wp-block-paragraph">Packages:</p>



<ul class="wp-block-list">
<li><code>ElBruno.AgentsOrchestration.Abstractions</code></li>



<li><code>ElBruno.AgentsOrchestration.Orchestration</code></li>
</ul>



<p class="wp-block-paragraph">Repo:</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/elbruno.agentsorchestration">https://github.com/elbruno/elbruno.agentsorchestration</a></p>



<p class="wp-block-paragraph">This is useful when you want agents to do more than just reply to a prompt. The orchestration repo describes a 6-step pipeline:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Plan → Parse → Execute → Verify → Review → Report</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That is a nice mental model for agent-based workflows. It also fits very well with GitHub Copilot, SQUAD-style automation, and repo-based development experiments.</p>



<p class="wp-block-paragraph">Because once you have more than one agent, you need orchestration.</p>



<p class="wp-block-paragraph">Otherwise, congratulations, you invented a very expensive group chat.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">AOT-friendly mapping for .NET</h2>



<p class="wp-block-paragraph">The <code>ElBruno.AotMapper</code> family is focused on compile-time DTO mapping.</p>



<p class="wp-block-paragraph">Packages:</p>



<ul class="wp-block-list">
<li><code>ElBruno.AotMapper</code></li>



<li><code>ElBruno.AotMapper.AspNetCore</code></li>



<li><code>ElBruno.AotMapper.EntityFramework</code></li>



<li><code>ElBruno.AotMapper.Generator</code></li>
</ul>



<p class="wp-block-paragraph">Repo:</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.AotMapper">https://github.com/elbruno/ElBruno.AotMapper</a></p>



<p class="wp-block-paragraph">This project is especially interesting for modern .NET workloads because it avoids runtime reflection and generates mapping code at compile time.</p>



<p class="wp-block-paragraph">That makes it useful for:</p>



<ul class="wp-block-list">
<li>NativeAOT apps</li>



<li>trimmed applications</li>



<li>cloud-native workloads</li>



<li>serverless scenarios</li>



<li>places where startup time and predictability matter</li>
</ul>



<p class="wp-block-paragraph">In short: less runtime magic, more generated code that you can actually inspect.</p>



<p class="wp-block-paragraph">And sometimes that is exactly what you want.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Local embeddings</h2>



<p class="wp-block-paragraph">The <code>ElBruno.LocalEmbeddings</code> family is one of the most useful groups for AI developers building RAG, semantic search, local-first AI apps, or privacy-aware demos.</p>



<p class="wp-block-paragraph">Packages include:</p>



<ul class="wp-block-list">
<li><code>ElBruno.LocalEmbeddings</code></li>



<li><code>ElBruno.LocalEmbeddings.Harrier</code></li>



<li><code>ElBruno.LocalEmbeddings.ImageEmbeddings</code></li>



<li><code>ElBruno.LocalEmbeddings.ImageEmbeddings.Downloader</code></li>



<li><code>ElBruno.LocalEmbeddings.KernelMemory</code></li>



<li><code>ElBruno.LocalEmbeddings.Npu</code></li>



<li><code>ElBruno.LocalEmbeddings.Npu.Intel</code></li>



<li><code>ElBruno.LocalEmbeddings.Npu.Qualcomm</code></li>



<li><code>ElBruno.LocalEmbeddings.VectorData</code></li>
</ul>



<p class="wp-block-paragraph">Repo:</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/elbruno.localembeddings">https://github.com/elbruno/elbruno.localembeddings</a></p>



<p class="wp-block-paragraph">This family gives you local embedding generation in .NET, including support for text embeddings, image embeddings, vector data integrations, Kernel Memory, and NPU-specific packages.</p>



<p class="wp-block-paragraph">The NPU packages are especially cool because they connect directly with the AI PC story.</p>



<p class="wp-block-paragraph">Not every embedding call needs to cross the internet.</p>



<p class="wp-block-paragraph">Sometimes the best cloud call is the one you did not make.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Local LLMs</h2>



<p class="wp-block-paragraph">The <code>ElBruno.LocalLLMs</code> packages are focused on running and integrating local language models from .NET.</p>



<p class="wp-block-paragraph">Packages:</p>



<ul class="wp-block-list">
<li><code>ElBruno.LocalLLMs</code></li>



<li><code>ElBruno.LocalLLMs.BitNet</code></li>



<li><code>ElBruno.LocalLLMs.Rag</code></li>
</ul>



<p class="wp-block-paragraph">Repo:</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.LocalLLMs">https://github.com/elbruno/ElBruno.LocalLLMs</a></p>



<p class="wp-block-paragraph">These are useful for local chat, local model experimentation, and RAG-style workflows.</p>



<p class="wp-block-paragraph">Cloud models are amazing. But sometimes you want the model running right there next to your code, your logs, your fan noise, and your questionable coffee.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">MarkItDotNet: from files to AI-ready Markdown</h2>



<p class="wp-block-paragraph">The <code>ElBruno.MarkItDotNet</code> family is probably one of the biggest and most useful areas in this package collection.</p>



<p class="wp-block-paragraph">Packages include:</p>



<ul class="wp-block-list">
<li><code>ElBruno.MarkItDotNet</code></li>



<li><code>ElBruno.MarkItDotNet.AI</code></li>



<li><code>ElBruno.MarkItDotNet.AzureSearch</code></li>



<li><code>ElBruno.MarkItDotNet.Chunking</code></li>



<li><code>ElBruno.MarkItDotNet.Citations</code></li>



<li><code>ElBruno.MarkItDotNet.Cli</code></li>



<li><code>ElBruno.MarkItDotNet.CoreModel</code></li>



<li><code>ElBruno.MarkItDotNet.DocumentIntelligence</code></li>



<li><code>ElBruno.MarkItDotNet.Excel</code></li>



<li><code>ElBruno.MarkItDotNet.Metadata</code></li>



<li><code>ElBruno.MarkItDotNet.PowerPoint</code></li>



<li><code>ElBruno.MarkItDotNet.Quality</code></li>



<li><code>ElBruno.MarkItDotNet.Sync</code></li>



<li><code>ElBruno.MarkItDotNet.VectorData</code></li>



<li><code>ElBruno.MarkItDotNet.Whisper</code></li>
</ul>



<p class="wp-block-paragraph">Repo:</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></p>



<p class="wp-block-paragraph">This family is about converting, preparing, enriching, chunking, indexing, validating, and syncing content for AI workflows.</p>



<p class="wp-block-paragraph">In a typical RAG project, everyone wants the fancy chat UI.</p>



<p class="wp-block-paragraph">But before that, someone has to solve the real problem:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Can we clean and prepare the documents first?</p>
</blockquote>



<p class="wp-block-paragraph">That is where this package family fits.</p>



<p class="wp-block-paragraph">It helps turn files into AI-ready Markdown and supports scenarios like document conversion, chunking, metadata extraction, citations, Azure AI Search, Whisper transcription, and quality checks.</p>



<p class="wp-block-paragraph">Because every RAG project eventually becomes a document-cleanup project wearing an AI hat.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Text-to-image generation</h2>



<p class="wp-block-paragraph">The <code>ElBruno.Text2Image</code> packages provide a .NET-friendly way to work with image generation.</p>



<p class="wp-block-paragraph">Packages:</p>



<ul class="wp-block-list">
<li><code>ElBruno.Text2Image</code></li>



<li><code>ElBruno.Text2Image.Cli</code></li>



<li><code>ElBruno.Text2Image.Cpu</code></li>



<li><code>ElBruno.Text2Image.Cuda</code></li>



<li><code>ElBruno.Text2Image.DirectML</code></li>



<li><code>ElBruno.Text2Image.Foundry</code></li>
</ul>



<p class="wp-block-paragraph">Repo:</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.Text2Image">https://github.com/elbruno/ElBruno.Text2Image</a></p>



<p class="wp-block-paragraph">The nice part here is the multi-backend design:</p>



<ul class="wp-block-list">
<li>CPU</li>



<li>CUDA</li>



<li>DirectML</li>



<li>Microsoft Foundry</li>



<li>CLI</li>
</ul>



<p class="wp-block-paragraph">That gives you flexibility depending on the machine, the demo, the budget, and how much your GPU is already crying.</p>



<p class="wp-block-paragraph">One API, multiple ways to make pixels appear like magic.</p>



<p class="wp-block-paragraph">Very expensive magic sometimes, but still magic.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Realtime audio, Whisper, and voice</h2>



<p class="wp-block-paragraph">This group is about speech, transcription, realtime audio, text-to-speech, and voice scenarios.</p>



<p class="wp-block-paragraph">Packages:</p>



<ul class="wp-block-list">
<li><code>ElBruno.Whisper</code></li>



<li><code>ElBruno.Realtime</code></li>



<li><code>ElBruno.Realtime.SileroVad</code></li>



<li><code>ElBruno.Realtime.Whisper</code></li>



<li><code>ElBruno.QwenTTS</code></li>



<li><code>ElBruno.QwenTTS.VoiceCloning</code></li>



<li><code>ElBruno.VibeVoiceTTS</code></li>



<li><code>ElBruno.PersonaPlex</code></li>
</ul>



<p class="wp-block-paragraph">Repos:</p>



<p class="wp-block-paragraph"><a><a href="https://github.com/elbruno/ElBruno.Whisper" rel="nofollow">https://github.com/elbruno/ElBruno.Whisper</a></a><br /><a href="https://github.com/elbruno/ElBruno.Realtime?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.Realtime</a><br /><a href="https://github.com/elbruno/ElBruno.QwenTTS">https://github.com/elbruno/ElBruno.QwenTTS</a><br /><a href="https://github.com/elbruno/ElBruno.VibeVoiceTTS">https://github.com/elbruno/ElBruno.VibeVoiceTTS</a><br /><a><a href="https://github.com/elbruno/ElBruno.PersonaPlex" rel="nofollow">https://github.com/elbruno/ElBruno.PersonaPlex</a></a></p>



<p class="wp-block-paragraph">This is useful for voice agents, transcription tools, accessibility scenarios, podcast workflows, and realtime conversational demos.</p>



<p class="wp-block-paragraph">Because sooner or later every AI demo becomes:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Can I talk to it?</p>
</blockquote>



<p class="wp-block-paragraph">And then, ten minutes later:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Why is my microphone still open?</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">QR code generation</h2>



<p class="wp-block-paragraph">The <code>ElBruno.QRCodeGenerator</code> family is a lightweight utility set for generating QR codes in different formats.</p>



<p class="wp-block-paragraph">Packages:</p>



<ul class="wp-block-list">
<li><code>ElBruno.QRCodeGenerator.Ascii</code></li>



<li><code>ElBruno.QRCodeGenerator.CLI</code></li>



<li><code>ElBruno.QRCodeGenerator.Image</code></li>



<li><code>ElBruno.QRCodeGenerator.Payloads</code></li>



<li><code>ElBruno.QRCodeGenerator.Pdf</code></li>



<li><code>ElBruno.QRCodeGenerator.Svg</code></li>



<li><code>ElBruno.QRCodeGenerator.Tool</code></li>
</ul>



<p class="wp-block-paragraph">Repos:</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.QRCodeGenerator?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.QRCodeGenerator</a><br /><a><a href="https://github.com/elbruno/ElBruno.QRCodeGenerator.CLI" rel="nofollow">https://github.com/elbruno/ElBruno.QRCodeGenerator.CLI</a></a></p>



<p class="wp-block-paragraph">This one is very practical: terminal output, images, SVG, PDF, payloads, and a global tool.</p>



<p class="wp-block-paragraph">Because every conference session eventually needs a QR code.</p>



<p class="wp-block-paragraph">Usually five minutes before going live.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">MCP and tool routing</h2>



<p class="wp-block-paragraph">The <code>ElBruno.ModelContextProtocol.MCPToolRouter</code> package focuses on smarter tool selection for Model Context Protocol scenarios.</p>



<p class="wp-block-paragraph">Package:</p>



<ul class="wp-block-list">
<li><code>ElBruno.ModelContextProtocol.MCPToolRouter</code></li>
</ul>



<p class="wp-block-paragraph">Repo:</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.ModelContextProtocol">https://github.com/elbruno/ElBruno.ModelContextProtocol</a></p>



<p class="wp-block-paragraph">This is useful when working with agents that have access to many tools.</p>



<p class="wp-block-paragraph">Sending every possible tool to the model all the time is not always a good idea. It burns tokens, increases noise, and makes the model work harder than needed.</p>



<p class="wp-block-paragraph">Tool routing helps select the most relevant tools for the task.</p>



<p class="wp-block-paragraph">Agents are great.</p>



<p class="wp-block-paragraph">Agents with 97 tools in context are a token bonfire.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Search, ranking, and retrieval</h2>



<p class="wp-block-paragraph">These packages help with retrieval, ranking, and search scenarios.</p>



<p class="wp-block-paragraph">Packages:</p>



<ul class="wp-block-list">
<li><code>ElBruno.BM25</code></li>



<li><code>ElBruno.Reranking</code></li>



<li><code>graphify-dotnet</code></li>
</ul>



<p class="wp-block-paragraph">Repos:</p>



<p class="wp-block-paragraph"><a><a href="https://github.com/ElBruno/ElBruno.BM25" rel="nofollow">https://github.com/ElBruno/ElBruno.BM25</a></a><br /><a><a href="https://github.com/elbruno/ElBruno.Reranking" rel="nofollow">https://github.com/elbruno/ElBruno.Reranking</a></a><br /><a href="https://github.com/elbruno/graphify-dotnet">https://github.com/elbruno/graphify-dotnet</a></p>



<p class="wp-block-paragraph">This group fits nicely into RAG and knowledge discovery workflows.</p>



<p class="wp-block-paragraph">You need search.</p>



<p class="wp-block-paragraph">Then you need better search.</p>



<p class="wp-block-paragraph">Then you need reranking.</p>



<p class="wp-block-paragraph">Then you need graphs.</p>



<p class="wp-block-paragraph">Then you realize your “simple chatbot” is now a distributed knowledge system.</p>



<p class="wp-block-paragraph">Classic.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Developer desktop utilities</h2>



<p class="wp-block-paragraph">These are small tools that make developer life better.</p>



<p class="wp-block-paragraph">Packages:</p>



<ul class="wp-block-list">
<li><code>ElBruno.AspireMonitor</code></li>



<li><code>ElBruno.ClockTray</code></li>



<li><code>ElBruno.OllamaMonitor</code></li>
</ul>



<p class="wp-block-paragraph">Repos:</p>



<p class="wp-block-paragraph"><a><a href="https://github.com/elbruno/ElBruno.AspireMonitor" rel="nofollow">https://github.com/elbruno/ElBruno.AspireMonitor</a></a><br /><a><a href="https://github.com/elbruno/ElBruno.ClockTray" rel="nofollow">https://github.com/elbruno/ElBruno.ClockTray</a></a><br /><a href="https://github.com/elbruno/ElBruno.OllamaMonitor?utm_source=chatgpt.com">https://github.com/elbruno/ElBruno.OllamaMonitor</a></p>



<p class="wp-block-paragraph">These are the kind of tools that start with:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">I just need a tiny helper.</p>
</blockquote>



<p class="wp-block-paragraph">And then become useful enough to publish.</p>



<p class="wp-block-paragraph"><code>AspireMonitor</code> helps with Aspire monitoring.<br /><code>ClockTray</code> helps with Windows tray clock scenarios.<br /><code>OllamaMonitor</code> gives quick visibility into local Ollama runtime status.</p>



<p class="wp-block-paragraph">Tiny tools. Big quality-of-life improvement.</p>



<p class="wp-block-paragraph">The best kind of yak shaving.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Hugging Face helpers</h2>



<p class="wp-block-paragraph">The Hugging Face downloader packages help with downloading models and related assets from Hugging Face.</p>



<p class="wp-block-paragraph">Packages:</p>



<ul class="wp-block-list">
<li><code>ElBruno.HuggingFace.Downloader</code></li>



<li><code>ElBruno.HuggingFace.Downloader.Cli</code></li>
</ul>



<p class="wp-block-paragraph">Repo:</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.HuggingFace.Downloader">https://github.com/elbruno/ElBruno.HuggingFace.Downloader</a></p>



<p class="wp-block-paragraph">This is useful across many of the local AI packages, because local AI usually starts with:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Great, now where do I get the model files?</p>
</blockquote>



<p class="wp-block-paragraph">And then:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Why is this model 4 GB?</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">OllamaSharp extensions</h2>



<p class="wp-block-paragraph">Package:</p>



<ul class="wp-block-list">
<li><code>ElBruno.OllamaSharp.Extensions</code></li>
</ul>



<p class="wp-block-paragraph">Repo:</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/elbruno.OllamaSharp.Extensions">https://github.com/elbruno/elbruno.OllamaSharp.Extensions</a></p>



<p class="wp-block-paragraph">This package adds useful extensions around OllamaSharp, especially for scenarios where local LLM calls can take longer and timeout management becomes important.</p>



<p class="wp-block-paragraph">If you have ever waited for a local model to respond and wondered whether it was thinking, frozen, or silently judging your prompt, this one makes sense.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">MemPalace: memory for AI apps</h2>



<p class="wp-block-paragraph">The <code>MemPalace</code> family is focused on memory infrastructure for AI apps and agents.</p>



<p class="wp-block-paragraph">Packages:</p>



<ul class="wp-block-list">
<li><code>MemPalace.Core</code></li>



<li><code>MemPalace.Ai</code></li>



<li><code>MemPalace.Agents</code></li>



<li><code>MemPalace.Backends.Sqlite</code></li>



<li><code>MemPalace.KnowledgeGraph</code></li>



<li><code>MemPalace.Mcp</code></li>



<li><code>MemPalace.Mining</code></li>



<li><code>MemPalace.Search</code></li>



<li><code>mempalacenet</code></li>



<li><code>mempalacenet-bench</code></li>
</ul>



<p class="wp-block-paragraph">Repo:</p>



<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.MempalaceNet">https://github.com/elbruno/ElBruno.MempalaceNet</a></p>



<p class="wp-block-paragraph">This is about local-first AI memory, storage, search, knowledge graphs, agents, MCP integration, mining, and benchmarking.</p>



<p class="wp-block-paragraph">Because agents without memory are just very confident goldfish.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Closing thoughts</h2>



<p class="wp-block-paragraph">Most of these packages started as small ideas, experiments, demos, or helper libraries.</p>



<p class="wp-block-paragraph">But this is one of the things I love about modern development with GitHub Copilot: it makes it easier to move from:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">This works on my machine</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">to:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">This is packaged, documented, published, open source, and maybe useful for someone else.</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Are all of these perfect?</p>



<p class="wp-block-paragraph">Of course not.</p>



<p class="wp-block-paragraph">Are they useful?</p>



<p class="wp-block-paragraph">I hope so.</p>



<p class="wp-block-paragraph">Are they free, open source, and ready for you to try, break, improve, fork, complain about, and maybe even use in a real project?</p>



<p class="wp-block-paragraph">Yes. That is the idea.</p>



<p class="wp-block-paragraph">And as always:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Today, code is cheap. The decisions are expensive.</p>
</blockquote>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/05/10/from-pet-projects-to-20-open-source-nuget-packages-thanks-to-github-copilot-%f0%9f%9a%80/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40311</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-10-2026-08_03_42-am-small.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-10-2026-08_03_42-am-small.png">
			<media:title type="html">ChatGPT Image May 10, 2026, 08_03_42 AM-small</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-10-2026-08_03_42-am-small.png?w=836"/>
	</item>
		<item>
		<title>Running GitHub Copilot CLI Offline with Local Models: GPU Edition</title>
		<link>https://elbruno.com/2026/05/06/running-github-copilot-cli-offline-with-local-models-gpu-edition/</link>
					<comments>https://elbruno.com/2026/05/06/running-github-copilot-cli-offline-with-local-models-gpu-edition/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Wed, 06 May 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40289</guid>

					<description><![CDATA[TL;DR After my first CPU-only experiment with GitHub Copilot CLI and local models, I wanted to try the same idea again, but this time with a GPU-powered setup. The first experiment proved that running GitHub Copilot CLI offline with local models was technically possible. This second experiment showed something more interesting: With the right model, [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<h2 class="wp-block-heading">TL;DR</h2>



<p class="wp-block-paragraph">After my first CPU-only experiment with GitHub Copilot CLI and local models, I wanted to try the same idea again, but this time with a GPU-powered setup.</p>



<p class="wp-block-paragraph">The first experiment proved that running GitHub Copilot CLI offline with local models was technically possible.</p>



<p class="wp-block-paragraph">This second experiment showed something more interesting:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">With the right model, GPU acceleration, small bounded phases, and strict human checkpoints, local agentic coding can become practical enough to move a real .NET project forward.</p>
</blockquote>



<p class="wp-block-paragraph">The setup:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">GitHub Copilot CLI<br />LM Studio<br />Qwen3.6 35B A3B<br />GPU acceleration<br />.NET 10<br />PowerShell<br />GitHub CLI<br />GitHub Actions</p>
</blockquote>



<p class="wp-block-paragraph">The project:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">ElBruno.NetAgent<br />A Windows-first .NET network utility, built with dry-run safety first.</p>
</blockquote>



<p class="wp-block-paragraph">Repository:</p>



<p class="wp-block-paragraph"><a href="https://github.com/brunoghcpft-oss/ElBruno.NetAgent">https://github.com/brunoghcpft-oss/ElBruno.NetAgent</a></p>



<p class="wp-block-paragraph">Previous CPU-only post:</p>



<figure class="wp-block-embed is-type-wp-embed is-provider-el-bruno wp-block-embed-el-bruno"><div class="wp-block-embed__wrapper">
<blockquote class="wp-embedded-content" data-secret="oOOsFOzVpA"><a href="https://elbruno.com/2026/05/03/running-github-copilot-cli-offline-with-local-models-a-cpu-only-reality-check/">Running GitHub Copilot CLI Offline with Local Models: A CPU-Only Reality&nbsp;Check</a></blockquote><iframe loading="lazy" class="wp-embedded-content" sandbox="allow-scripts" security="restricted"  title="&#8220;Running GitHub Copilot CLI Offline with Local Models: A CPU-Only Reality&nbsp;Check&#8221; &#8212; El Bruno" src="https://elbruno.com/2026/05/03/running-github-copilot-cli-offline-with-local-models-a-cpu-only-reality-check/embed/#?secret=Rzx7LeoKuj#?secret=oOOsFOzVpA" data-secret="oOOsFOzVpA" width="500" height="282" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe>
</div></figure>



<p class="wp-block-paragraph">The short version:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Cloud models would have been much faster.<br />Local mode took more than a day.<br />But the local workflow actually worked.<br />And the lessons were worth it.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">From CPU-only reality check to GPU-assisted local agents</h2>



<p class="wp-block-paragraph">In the first experiment, I tested GitHub Copilot CLI offline using local models on a CPU-only environment.</p>



<p class="wp-block-paragraph">That test was useful, but also humbling.</p>



<p class="wp-block-paragraph">The model could reason, but it was slow. Broad prompts were painful. Asking something like:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">How does this repository work?</p>
</blockquote>



<p class="wp-block-paragraph">was not a good idea. The model had too much to process, tool calls became expensive, and long-running attempts could easily end in timeouts or partial answers.</p>



<p class="wp-block-paragraph">So the first big lesson was simple:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Offline local agentic coding is possible, but CPU-only is a reality check.</p>
</blockquote>



<p class="wp-block-paragraph">This time I wanted to answer a different question:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">What changes when we add GPU acceleration?</p>
</blockquote>



<p class="wp-block-paragraph">Not in a synthetic benchmark. Not in a “hello world” repo. But in a real .NET project with tests, CI, packaging, docs, safety constraints, and GitHub Actions.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The setup</h2>



<p class="wp-block-paragraph">The local provider was&nbsp;<strong>LM Studio</strong>, exposing an OpenAI-compatible endpoint.</p>



<p class="wp-block-paragraph">The model was:</p>



<p class="wp-block-paragraph">qwen/qwen3.6-35b-a3b</p>



<p class="wp-block-paragraph">LM Studio was configured with a large context window:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Context length: 262,144 tokens<br />GPU offload enabled<br />Unified KV cache enabled<br />Model kept in memory</p>
</blockquote>



<p class="wp-block-paragraph">For this GPU experiment, I used a <strong>Microsoft Dev Box</strong> with an <strong>NVIDIA Tesla T4 GPU with 16 GB of VRAM</strong>, an <strong>AMD EPYC 7V12 64-core processor</strong>, and <strong>110 GB of RAM</strong>. This was not a high-end consumer GPU workstation, but it was enough to make the local Copilot CLI + LM Studio + Qwen workflow practical. Compared with the CPU-only experiment, the difference was very noticeable, even though powerful cloud models would still be significantly faster.</p>



<p class="wp-block-paragraph">Copilot CLI was configured to use the local LM Studio endpoint:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">cd</span> <span class="tok-variableName">C:</span><span class="tok-punctuation">\</span><span class="tok-variableName">src</span><span class="tok-punctuation">\</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_OFFLINE</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;true&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_TYPE</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;openai&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_BASE_URL</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;http://localhost:1234/v1&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_MODEL</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;qwen/qwen3.6-35b-a3b&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Depending on the task, I adjusted the context and output limits.</p>



<p class="wp-block-paragraph">For diagnostic tasks with logs:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_MAX_PROMPT_TOKENS</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;32768&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_MAX_OUTPUT_TOKENS</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;1536&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">For large documentation or release-readiness tasks:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_MAX_PROMPT_TOKENS</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;131072&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_MAX_OUTPUT_TOKENS</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;4096&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">For full repo audit or very large synthesis:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_MAX_PROMPT_TOKENS</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;196608&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_MAX_OUTPUT_TOKENS</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;4096&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Or, when really needed:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_MAX_PROMPT_TOKENS</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;262144&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_MAX_OUTPUT_TOKENS</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;4096&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">But I quickly learned not to use maximum context by default.</p>



<p class="wp-block-paragraph">More context is not always better.</p>



<p class="wp-block-paragraph">More context can also mean:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">slower responses<br />more token burn<br />more chance of runaway generation<br />more opportunity for the model to overthink<br />more tool-call noise</p>
</blockquote>



<p class="wp-block-paragraph">The short rule became:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Small context for precision. Large context for synthesis. Max context only when the task truly needs it.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Launching Copilot CLI offline</h2>



<p class="wp-block-paragraph">For most implementation phases, I launched Copilot CLI like this:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">copilot</span> <span class="tok-operator">--</span><span class="tok-variableName">no-remote</span> <span class="tok-operator">--</span><span class="tok-variableName">disable-builtin-mcps</span> <span class="tok-operator">--</span><span class="tok-variableName">stream</span> <span class="tok-variableName">on</span> <span class="tok-operator">--</span><span class="tok-variableName">max-autopilot-continues</span> <span class="tok-number">6</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">When file edits were needed, I enabled full tool permissions inside Copilot CLI:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">/allow-all on</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">And verified with:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">/allow-all show</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">This mattered a lot.</p>



<p class="wp-block-paragraph">Without&nbsp;<code>/allow-all on</code>, Copilot CLI could read files and run read-only commands, but file-edit tools were blocked with errors like:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Permission denied and could not request permission from user</p>
</blockquote>



<p class="wp-block-paragraph">After enabling&nbsp;<code>/allow-all on</code>, Copilot CLI was able to apply targeted changes.</p>



<p class="wp-block-paragraph">That became another useful rule:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Diagnosis mode:</p>



<ul class="wp-block-list">
<li>no yolo</li>



<li>smaller context</li>



<li>read files</li>



<li>read logs</li>



<li>analyze</li>



<li>stop</li>
</ul>



<p class="wp-block-paragraph">Implementation mode:</p>



<ul class="wp-block-list">
<li>/allow-all on</li>



<li>constrained prompt</li>



<li>explicitly listed files</li>



<li>low max-autopilot-continues</li>



<li>validate</li>



<li>stop</li>
</ul>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The project: ElBruno.NetAgent</h2>



<p class="wp-block-paragraph">The project used for the experiment was&nbsp;<strong>ElBruno.NetAgent</strong>:</p>



<p class="wp-block-paragraph"><a href="https://github.com/brunoghcpft-oss/ElBruno.NetAgent">https://github.com/brunoghcpft-oss/ElBruno.NetAgent</a></p>



<p class="wp-block-paragraph">The goal of the app is to become a Windows-first network utility that can monitor network interfaces, evaluate connection quality, and eventually help with network-switching decisions.</p>



<p class="wp-block-paragraph">The important part: during this experiment, the app stayed&nbsp;<strong>dry-run only</strong>.</p>



<p class="wp-block-paragraph">No real network mutation. No actual interface metric changes. No live switching. No NuGet publishing.</p>



<p class="wp-block-paragraph">This was intentional. Since I was letting Copilot CLI and a local model modify code, scripts, tests, docs, and CI, safety constraints were non-negotiable.</p>



<p class="wp-block-paragraph">The app had to stay in this mode:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Dry-run only<br />Live mode blocked by default<br />Unsafe config rejected<br />Audit logs for simulated actions<br />No real network mutation</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">What the local model actually helped build</h2>



<p class="wp-block-paragraph">This was not a single prompt experiment.</p>



<p class="wp-block-paragraph">It was a sequence of bounded phases.</p>



<p class="wp-block-paragraph">Some of the validated phases included:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Phase 6: Network controller dry-run foundation<br />Phase 7: Audit log for simulated controller actions<br />Phase 8: Tray UI status + audit log viewer<br />Phase 9: Packaging validation<br />Phase 10: Safety guardrails<br />Phase 11: NuGet package icon with t2i<br />Phase 12: Local package smoke test<br />Phase 13: CI packaging validation<br />Phase 14: CI test isolation fix</p>
</blockquote>



<p class="wp-block-paragraph">By the end, the project had:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">.NET 10 project structure<br />WPF tray shell<br />configuration service<br />network inventory foundation<br />network quality monitor<br />decision engine<br />dry-run network controller<br />audit log service<br />UI status surface<br />NuGet metadata<br />package icon<br />local package smoke test<br />GitHub Actions CI validation<br />safety guardrails<br />125 passing tests</p>
</blockquote>



<p class="wp-block-paragraph">That is the part that made this experiment interesting.</p>



<p class="wp-block-paragraph">The local model was not just generating sample code. It was participating in a real workflow.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Model choice mattered</h2>



<p class="wp-block-paragraph">This experiment also reminded me that “local model” is not a single category.</p>



<p class="wp-block-paragraph">I originally started experimenting with&nbsp;<strong>Gemma 4</strong>, but eventually switched to&nbsp;<strong>Qwen3.6 35B A3B</strong>.</p>



<p class="wp-block-paragraph">In this setup, Qwen behaved better.</p>



<p class="wp-block-paragraph">It felt more disciplined for bounded tasks. It handled tool-assisted workflows better. It was also faster in the specific LM Studio + GPU configuration I was using.</p>



<p class="wp-block-paragraph">That does not mean Qwen is universally better than Gemma.</p>



<p class="wp-block-paragraph">It means that for this specific setup:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">GitHub Copilot CLI<br />LM Studio<br />large-context local model<br />.NET repo<br />PowerShell tools<br />GitHub CLI<br />bounded coding tasks</p>
</blockquote>



<p class="wp-block-paragraph">Qwen was the model that made the workflow practical.</p>



<p class="wp-block-paragraph">This is an important point: local agentic coding is not just about having “a local model.”</p>



<p class="wp-block-paragraph">The specific model matters. The runtime matters. The prompt matters. The tool orchestration matters.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Cloud would have been faster. That was not the point.</h2>



<p class="wp-block-paragraph">Let’s be honest.</p>



<p class="wp-block-paragraph">A powerful cloud model could probably have completed most of this much faster.</p>



<p class="wp-block-paragraph">My guess: with a strong cloud model, good repo context, and the same human guidance, many of these phases could probably be completed in one or two hours.</p>



<p class="wp-block-paragraph">The local GPU workflow took more than a day.</p>



<p class="wp-block-paragraph">There were restarts. There were prompt adjustments. There were permission issues. There were GitHub CLI authentication steps. There were runaway generations. There were context-tuning experiments. There were CI failures. There were moments where I had to stop the agent and take back control.</p>



<p class="wp-block-paragraph">So no, this was not faster than using a powerful cloud model.</p>



<p class="wp-block-paragraph">But speed was not the only variable I wanted to test.</p>



<p class="wp-block-paragraph">I wanted to understand:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Can Copilot CLI work with a local OpenAI-compatible provider?<br />Can a local model use tools well enough to move a real repo forward?<br />Can it diagnose GitHub Actions failures from logs?<br />Can it respect dry-run safety constraints?<br />Can it generate packaging assets?<br />Can it validate build, test, pack, and smoke-test flows?<br />How much human steering is required?<br />Where does the local workflow break?</p>
</blockquote>



<p class="wp-block-paragraph">The answer was:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Yes, it can work. But it needs constraints, checkpoints, and patience.</p>
</blockquote>



<p class="wp-block-paragraph">Local models are not yet the fastest path for this workflow.</p>



<p class="wp-block-paragraph">But they are becoming a realistic path for controlled, private, offline-capable experimentation.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The token burn is real</h2>



<p class="wp-block-paragraph">One of the most useful screenshots from the experiment showed a real Copilot CLI run with something like:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Duration: 1h 30m<br />Input tokens: ~1.8M<br />Output tokens: ~13.5K<br />Reasoning tokens: ~2.2K</p>
</blockquote>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="348" src="https://elbruno.com/wp-content/uploads/2026/05/image.png?w=1024" alt="" class="wp-image-40296" srcset="https://elbruno.com/wp-content/uploads/2026/05/image.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/05/image.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/image.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/05/image.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/05/image.png 1114w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">This is a very important part of the story.</p>



<p class="wp-block-paragraph">Local does not mean free.</p>



<p class="wp-block-paragraph">It means the cost moves somewhere else:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">your GPU<br />your CPU<br />your memory<br />your time<br />your electricity<br />your patience<br />your prompt discipline</p>
</blockquote>



<p class="wp-block-paragraph">Or, in a more honest way:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">The model does not charge per token, but your GPU, your laptop, and your coffee do have limits.</p>
</blockquote>



<p class="wp-block-paragraph">This changed how I structured the work.</p>



<p class="wp-block-paragraph">I stopped thinking in terms of “one big prompt.”</p>



<p class="wp-block-paragraph">Instead, I used phases.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Bounded phases were the real unlock</h2>



<p class="wp-block-paragraph">The most important lesson was not “use a GPU.”</p>



<p class="wp-block-paragraph">The most important lesson was:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Small, bounded phases work. Huge open-ended prompts fail.</p>
</blockquote>



<p class="wp-block-paragraph">Good prompts looked like this:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Implement only this phase.<br />Modify only these files.<br />Do not use agents.<br />Do not use read_agent.<br />Do not use SQL.<br />Do not publish.<br />Do not mutate network state.<br />Run this exact validation command.<br />Stop after the summary.</p>
</blockquote>



<p class="wp-block-paragraph">Bad prompts looked like this:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Analyze the whole repo and improve everything.<br />Make it production ready.<br />Fix all warnings.<br />Validate everything.<br />Continue until done.</p>
</blockquote>



<p class="wp-block-paragraph">The local model could handle focused tasks much better than open-ended ones. A good implementation prompt looked like this:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Apply this exact minimal fix.</p>



<p class="wp-block-paragraph">Do not diagnose.<br />Do not use agents.<br />Do not use read_agent.<br />Do not use SQL.<br />Do not use todo tracking.<br />Do not edit CI YAML.<br />Do not modify any files except:</p>



<ul class="wp-block-list">
<li>src\ElBruno.NetAgent\Services\Configuration\ConfigurationService.cs</li>



<li>tests\ElBruno.NetAgent.Tests\SafetyGuardrailTests.cs</li>
</ul>



<p class="wp-block-paragraph">If any write/edit tool fails with &#8220;Permission denied and could not request permission from user&#8221;, stop immediately. Do not try alternate write methods.</p>



<p class="wp-block-paragraph">Required change 1:<br />In ConfigurationService.cs, add an optional constructor parameter:<br />string? configDirectoryOverride = null</p>



<p class="wp-block-paragraph">Default behavior must remain unchanged.<br />If configDirectoryOverride is null, continue using:<br />Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)<br />combined with ElBruno.NetAgent.</p>



<p class="wp-block-paragraph">If configDirectoryOverride is provided, use that directory as the configuration directory.</p>



<p class="wp-block-paragraph">Required change 2:<br />In SafetyGuardrailTests.cs, update only the test:<br />SafetyChain_DefaultConfig_AllowsNoLiveExecution</p>



<p class="wp-block-paragraph">Make this test use a unique temp directory:<br />Path.Combine(Path.GetTempPath(), &#8220;ElBruno.NetAgent.Test-&#8221; + Guid.NewGuid().ToString(&#8220;N&#8221;))</p>



<p class="wp-block-paragraph">Create the directory before using it.<br />Pass it to the ConfigurationService constructor.<br />Clean it up in finally if practical.</p>



<p class="wp-block-paragraph">Validation:<br />Run only:<br />dotnet test .\tests\ElBruno.NetAgent.Tests\ElBruno.NetAgent.Tests.csproj &#8211;configuration Release &#8211;verbosity minimal</p>



<p class="wp-block-paragraph">After validation, summarize:</p>



<ul class="wp-block-list">
<li>exact files changed</li>



<li>test result</li>



<li>whether I should commit and push</li>
</ul>



<p class="wp-block-paragraph">Stop after summary.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Tool-assisted diagnosis worked better than full autopilot</h2>



<p class="wp-block-paragraph">One of the best parts of the experiment happened when GitHub Actions failed.</p>



<p class="wp-block-paragraph">Local validation passed:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">dotnet build: passed<br />dotnet test: 125/125 passed<br />dotnet pack: passed<br />local package smoke test: passed</p>
</blockquote>



<p class="wp-block-paragraph">But GitHub Actions failed in CI.</p>



<p class="wp-block-paragraph">Instead of asking Copilot CLI to “fix CI,” I gave it a diagnostic-only prompt:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Goal:<br />Diagnose a failing GitHub Actions workflow using minimal tool-assisted analysis.</p>



<p class="wp-block-paragraph">Use tools only to gather the minimum data:</p>



<ol class="wp-block-list">
<li>Read the local workflow:<br />Get-Content ..github\workflows\ci.yml</li>



<li>Read the test project file:<br />Get-Content .\tests\ElBruno.NetAgent.Tests\ElBruno.NetAgent.Tests.csproj</li>



<li>Read the app project file:<br />Get-Content .\src\ElBruno.NetAgent\ElBruno.NetAgent.csproj</li>



<li>Fetch the failed GitHub Actions job log:<br />gh run view 25378546671 &#8211;job 74420179140 &#8211;log-failed</li>
</ol>



<p class="wp-block-paragraph">After gathering data, return:</p>



<ol class="wp-block-list">
<li>exact failing evidence from the CI log</li>



<li>likely root cause</li>



<li>smallest YAML-only fix, if any</li>



<li>exact YAML lines to change</li>



<li>whether I should validate locally or push directly</li>
</ol>



<p class="wp-block-paragraph">Do not edit files.<br />Stop after the analysis.</p>
</blockquote>



<p class="wp-block-paragraph">This worked.</p>



<p class="wp-block-paragraph">Copilot CLI used the GitHub CLI log and found the real failure:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">System.IO.IOException:</div><div class="cm-line">The process cannot access the file</div><div class="cm-line">C:\Users\runneradmin\AppData\Local\ElBruno.NetAgent\config.json</div><div class="cm-line">because it is being used by another process.</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The failing test was:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-variableName">SafetyGuardrailTests</span>.<span class="tok-variableName">SafetyChain_DefaultConfig_AllowsNoLiveExecution</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The important part: Copilot correctly concluded that this was&nbsp;<strong>not</strong>&nbsp;a YAML problem.</p>



<p class="wp-block-paragraph">It was a test isolation issue.</p>



<p class="wp-block-paragraph">The test was using the real user config path under&nbsp;<code>%LOCALAPPDATA%</code>, and in CI that file could be locked by another test/process.</p>



<p class="wp-block-paragraph">The fix was to use a unique temporary config directory for that test.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The CI fix</h2>



<p class="wp-block-paragraph">The production behavior needed to stay unchanged.</p>



<p class="wp-block-paragraph">So the fix was small:</p>



<ol class="wp-block-list">
<li>Add an optional config directory override to&nbsp;<code>ConfigurationService</code>.</li>



<li>Keep the default path as&nbsp;<code>%LOCALAPPDATA%\ElBruno.NetAgent</code>.</li>



<li>In the failing test, pass a unique temp directory.</li>



<li>Clean it up after the test.</li>
</ol>



<p class="wp-block-paragraph">Conceptually:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">public</span> <span class="tok-variableName">ConfigurationService</span>(</div><div class="cm-line">    <span class="tok-variableName">ILogger</span><span class="tok-operator">&lt;</span><span class="tok-variableName">ConfigurationService</span><span class="tok-operator">&gt;</span> <span class="tok-variableName">logger</span>,</div><div class="cm-line">    <span class="tok-typeName">string</span><span class="tok-operator">?</span> <span class="tok-variableName">configDirectoryOverride</span> <span class="tok-operator">=</span> <span class="tok-atom">null</span>)</div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">_logger</span> <span class="tok-operator">=</span> <span class="tok-variableName">logger</span>;</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">baseDirectory</span> <span class="tok-operator">=</span> <span class="tok-variableName">configDirectoryOverride</span></div><div class="cm-line">        <span class="tok-operator">??</span> <span class="tok-variableName">Path</span>.<span class="tok-variableName">Combine</span>(</div><div class="cm-line">            <span class="tok-variableName">Environment</span>.<span class="tok-variableName">GetFolderPath</span>(<span class="tok-variableName">Environment</span>.<span class="tok-variableName">SpecialFolder</span>.<span class="tok-variableName">LocalApplicationData</span>),</div><div class="cm-line">            <span class="tok-string">&quot;ElBruno.NetAgent&quot;</span>);</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">_configurationDirectory</span> <span class="tok-operator">=</span> <span class="tok-variableName">baseDirectory</span>;</div><div class="cm-line">    <span class="tok-variableName">_configurationPath</span> <span class="tok-operator">=</span> <span class="tok-variableName">Path</span>.<span class="tok-variableName">Combine</span>(<span class="tok-variableName">_configurationDirectory</span>, <span class="tok-string">&quot;config.json&quot;</span>);</div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">And in the test:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">tempDirectory</span> <span class="tok-operator">=</span> <span class="tok-variableName">Path</span>.<span class="tok-variableName">Combine</span>(</div><div class="cm-line">    <span class="tok-variableName">Path</span>.<span class="tok-variableName">GetTempPath</span>(),</div><div class="cm-line">    <span class="tok-string">&quot;ElBruno.NetAgent.Test-&quot;</span> <span class="tok-operator">+</span> <span class="tok-typeName">Guid</span>.<span class="tok-variableName">NewGuid</span>().<span class="tok-variableName">ToString</span>(<span class="tok-string">&quot;N&quot;</span>));</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">Directory</span>.<span class="tok-variableName">CreateDirectory</span>(<span class="tok-variableName">tempDirectory</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">try</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">configService</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">ConfigurationService</span>(<span class="tok-variableName">_configLogger</span>, <span class="tok-variableName">tempDirectory</span>);</div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">options</span> <span class="tok-operator">=</span> <span class="tok-variableName">configService</span>.<span class="tok-variableName">LoadConfiguration</span>();</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-comment">// assertions...</span></div><div class="cm-line">}</div><div class="cm-line"><span class="tok-keyword">finally</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">if</span> (<span class="tok-variableName">Directory</span>.<span class="tok-variableName">Exists</span>(<span class="tok-variableName">tempDirectory</span>))</div><div class="cm-line">    {</div><div class="cm-line">        <span class="tok-variableName">Directory</span>.<span class="tok-variableName">Delete</span>(<span class="tok-variableName">tempDirectory</span>, <span class="tok-variableName">recursive</span>: <span class="tok-atom">true</span>);</div><div class="cm-line">    }</div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">After this fix:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Local test passed<br />GitHub Actions passed<br />CI packaging validation succeeded</p>
</blockquote>



<p class="wp-block-paragraph">That was a very nice moment in the experiment.</p>



<p class="wp-block-paragraph">The local model diagnosed a real CI failure using real logs, then helped apply a minimal fix.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Permissions mattered more than expected</h2>



<p class="wp-block-paragraph">Another lesson: tool permissions are part of the system.</p>



<p class="wp-block-paragraph">At one point, Copilot CLI could read files but could not write them.</p>



<p class="wp-block-paragraph">The logs showed repeated failures like:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Permission denied and could not request permission from user</p>
</blockquote>



<p class="wp-block-paragraph">It tried:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">edit tool<br />PowerShell write<br />Python write<br />Copy-Item<br />regex replace<br />backup file</p>
</blockquote>



<p class="wp-block-paragraph">That was a useful failure.</p>



<p class="wp-block-paragraph">It showed that the model understood the fix, but the Copilot CLI tool environment could not write files.</p>



<p class="wp-block-paragraph">After enabling:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">/allow-all on</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Copilot CLI could edit files successfully.</p>



<p class="wp-block-paragraph">But there was a tradeoff.</p>



<p class="wp-block-paragraph">With full permissions enabled, I had to watch the run more carefully.</p>



<p class="wp-block-paragraph">The practical rule became:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">If you see &#8220;Continuing autonomously&#8221; after the summary, stop it.<br />If you see read_agent, stop it.<br />If you see SQL/todo tracking, stop it.<br />If it touches files outside the allowed list, stop it.<br />If it tries to publish, stop it.</p>
</blockquote>



<p class="wp-block-paragraph">This is not “fire and forget.”</p>



<p class="wp-block-paragraph">This is human-guided automation.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Packaging, icon generation, and t2i</h2>



<p class="wp-block-paragraph">The experiment also included preparing the NuGet package.</p>



<p class="wp-block-paragraph">No publishing yet. Only validation.</p>



<p class="wp-block-paragraph">The package included:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Package metadata<br />README.md<br />NuGet icon<br />local .nupkg generation<br />static package smoke test<br />CI packaging validation</p>
</blockquote>



<p class="wp-block-paragraph">For the package icon, I used the local&nbsp;<code>t2i</code>&nbsp;skill and the&nbsp;<code>ElBruno.Text2Image.Cli</code>&nbsp;package:</p>



<p class="wp-block-paragraph"><a href="https://www.nuget.org/packages/ElBruno.Text2Image.Cli">https://www.nuget.org/packages/ElBruno.Text2Image.Cli</a></p>



<p class="wp-block-paragraph">The image generation prompt was something like:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Modern square app icon for &#8220;ElBruno.NetAgent&#8221;, a Windows-first network tray utility. Minimal semi-flat vector style. Central symbol combining Wi-Fi arcs, a small network switch/router, and a subtle shield/checkmark representing safe dry-run mode. Clean dark blue and cyan palette, high contrast, rounded square background, no text, no letters, no tiny details, readable at small NuGet package icon size.</p>
</blockquote>



<p class="wp-block-paragraph">The generated asset was wired into the project file as the NuGet package icon.</p>



<p class="wp-block-paragraph">The local package smoke test checked:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">package exists<br />README.md exists in package<br />icon exists in package<br />.nuspec contains icon metadata<br />packaged DLL exists<br />dry-run references exist</p>
</blockquote>



<p class="wp-block-paragraph">The smoke test was intentionally static.</p>



<p class="wp-block-paragraph">It did not launch the app. It did not mutate network state. It did not require admin permissions.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">CI packaging validation</h2>



<p class="wp-block-paragraph">The GitHub Actions workflow was created to validate packaging without publishing.</p>



<p class="wp-block-paragraph">It validates:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">restore<br />build<br />test<br />pack<br />local package smoke test<br />artifact upload</p>
</blockquote>



<p class="wp-block-paragraph">No secrets. No NuGet publishing. No app execution that mutates network state.</p>



<p class="wp-block-paragraph">A simplified version:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-yaml"><div class="cm-line"><span class="tok-propertyName tok-definition">name</span><span class="tok-punctuation">:</span> CI - Packaging Validation (No Publish)</div><div class="cm-line"></div><div class="cm-line"><span class="tok-propertyName tok-definition">on</span><span class="tok-punctuation">:</span></div><div class="cm-line">  <span class="tok-propertyName tok-definition">push</span><span class="tok-punctuation">:</span></div><div class="cm-line">    <span class="tok-propertyName tok-definition">branches</span><span class="tok-punctuation">:</span></div><div class="cm-line">      <span class="tok-punctuation">-</span> main</div><div class="cm-line">  <span class="tok-propertyName tok-definition">pull_request</span><span class="tok-punctuation">:</span></div><div class="cm-line">  <span class="tok-propertyName tok-definition">workflow_dispatch</span><span class="tok-punctuation">:</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-propertyName tok-definition">jobs</span><span class="tok-punctuation">:</span></div><div class="cm-line">  <span class="tok-propertyName tok-definition">packaging</span><span class="tok-punctuation">:</span></div><div class="cm-line">    <span class="tok-propertyName tok-definition">name</span><span class="tok-punctuation">:</span> CI Packaging Validation</div><div class="cm-line">    <span class="tok-propertyName tok-definition">runs-on</span><span class="tok-punctuation">:</span> windows-latest</div><div class="cm-line"></div><div class="cm-line">    <span class="tok-propertyName tok-definition">steps</span><span class="tok-punctuation">:</span></div><div class="cm-line">      <span class="tok-punctuation">-</span> <span class="tok-propertyName tok-definition">name</span><span class="tok-punctuation">:</span> Checkout</div><div class="cm-line">        <span class="tok-propertyName tok-definition">uses</span><span class="tok-punctuation">:</span> actions/checkout@v4</div><div class="cm-line"></div><div class="cm-line">      <span class="tok-punctuation">-</span> <span class="tok-propertyName tok-definition">name</span><span class="tok-punctuation">:</span> Setup .NET 10</div><div class="cm-line">        <span class="tok-propertyName tok-definition">uses</span><span class="tok-punctuation">:</span> actions/setup-dotnet@v4</div><div class="cm-line">        <span class="tok-propertyName tok-definition">with</span><span class="tok-punctuation">:</span></div><div class="cm-line">          <span class="tok-propertyName tok-definition">dotnet-version</span><span class="tok-punctuation">:</span> <span class="tok-string">&apos;10.0.x&apos;</span></div><div class="cm-line"></div><div class="cm-line">      <span class="tok-punctuation">-</span> <span class="tok-propertyName tok-definition">name</span><span class="tok-punctuation">:</span> Restore</div><div class="cm-line">        <span class="tok-propertyName tok-definition">run</span><span class="tok-punctuation">:</span> dotnet restore .\ElBruno.NetAgent.sln</div><div class="cm-line"></div><div class="cm-line">      <span class="tok-punctuation">-</span> <span class="tok-propertyName tok-definition">name</span><span class="tok-punctuation">:</span> Build</div><div class="cm-line">        <span class="tok-propertyName tok-definition">run</span><span class="tok-punctuation">:</span> dotnet build .\ElBruno.NetAgent.sln --configuration Release --no-restore</div><div class="cm-line"></div><div class="cm-line">      <span class="tok-punctuation">-</span> <span class="tok-propertyName tok-definition">name</span><span class="tok-punctuation">:</span> Test</div><div class="cm-line">        <span class="tok-propertyName tok-definition">run</span><span class="tok-punctuation">:</span> dotnet test .\ElBruno.NetAgent.sln --configuration Release --no-build --verbosity minimal</div><div class="cm-line"></div><div class="cm-line">      <span class="tok-punctuation">-</span> <span class="tok-propertyName tok-definition">name</span><span class="tok-punctuation">:</span> Pack</div><div class="cm-line">        <span class="tok-propertyName tok-definition">run</span><span class="tok-punctuation">:</span> dotnet pack .\src\ElBruno.NetAgent\ElBruno.NetAgent.csproj --configuration Release --no-build --output .\artifacts</div><div class="cm-line"></div><div class="cm-line">      <span class="tok-punctuation">-</span> <span class="tok-propertyName tok-definition">name</span><span class="tok-punctuation">:</span> Smoke test local package</div><div class="cm-line">        <span class="tok-propertyName tok-definition">shell</span><span class="tok-punctuation">:</span> pwsh</div><div class="cm-line">        <span class="tok-propertyName tok-definition">run</span><span class="tok-punctuation">:</span> .\scripts\smoke-test-local-package.ps1 -PackagePath .\artifacts\ElBruno.NetAgent.0.1.0.nupkg</div><div class="cm-line"></div><div class="cm-line">      <span class="tok-punctuation">-</span> <span class="tok-propertyName tok-definition">name</span><span class="tok-punctuation">:</span> Upload package artifact</div><div class="cm-line">        <span class="tok-propertyName tok-definition">uses</span><span class="tok-punctuation">:</span> actions/upload-artifact@v4</div><div class="cm-line">        <span class="tok-propertyName tok-definition">with</span><span class="tok-punctuation">:</span></div><div class="cm-line">          <span class="tok-propertyName tok-definition">name</span><span class="tok-punctuation">:</span> ElBruno.NetAgent-nupkg</div><div class="cm-line">          <span class="tok-propertyName tok-definition">path</span><span class="tok-punctuation">:</span> artifacts/*.nupkg</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">This gave the repo a safe release-readiness gate without publishing anything.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Safety guardrails were non-negotiable</h2>



<p class="wp-block-paragraph">Because this project is about network behavior, safety had to be part of the experiment from the beginning.</p>



<p class="wp-block-paragraph">The model was repeatedly instructed:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Do not execute real network changes.<br />Keep controller behavior dry-run only.<br />Do not publish to NuGet.<br />Do not add live network mutation.<br />Do not change safety behavior.</p>
</blockquote>



<p class="wp-block-paragraph">The app added guardrails such as:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">DryRunMode = true by default<br />LiveModeAllowed = false by default<br />unsafe config rejected<br />audit logs mark dry-run actions<br />live mode blocked<br />network mutation not implemented</p>
</blockquote>



<p class="wp-block-paragraph">The tests also checked these behaviors.</p>



<p class="wp-block-paragraph">That was important because local agentic coding is powerful, but the app domain matters.</p>



<p class="wp-block-paragraph">If the app can affect a machine’s network behavior, dry-run safety is not optional.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Prompt samples that worked well</h2>



<h3 class="wp-block-heading">1. Diagnostic prompt for GitHub Actions</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">We are continuing the ElBruno.NetAgent project.</p>



<p class="wp-block-paragraph">Goal:<br />Diagnose a failing GitHub Actions workflow using minimal tool-assisted analysis.</p>



<p class="wp-block-paragraph">Repo:<br />C:\src\ElBruno.NetAgent</p>



<p class="wp-block-paragraph">Failed GitHub Actions job:<br /><a href="https://github.com/brunoghcpft-oss/ElBruno.NetAgent/actions/runs//job/" rel="nofollow">https://github.com/brunoghcpft-oss/ElBruno.NetAgent/actions/runs//job/</a></p>



<p class="wp-block-paragraph">Important:</p>



<ul class="wp-block-list">
<li>gh auth is configured.</li>



<li>Do not edit files.</li>



<li>Do not run build/test/pack.</li>



<li>Do not use SQUAD.</li>



<li>Do not use task agents.</li>



<li>Do not use read_agent.</li>



<li>Do not use SQL or todo tracking.</li>



<li>Do not continue autonomously after the analysis.</li>



<li>Do not propose fixes without citing exact evidence from the CI log.</li>



<li>Use at most 4 shell commands.</li>



<li>Stop after the analysis.</li>
</ul>



<p class="wp-block-paragraph">Run only these commands:</p>



<ol class="wp-block-list">
<li>Get-Content ..github\workflows\ci.yml</li>



<li>Get-Content .\tests\ElBruno.NetAgent.Tests\ElBruno.NetAgent.Tests.csproj</li>



<li>Get-Content .\src\ElBruno.NetAgent\ElBruno.NetAgent.csproj</li>



<li>gh run view &#8211;job &#8211;log-failed</li>
</ol>



<p class="wp-block-paragraph">Return only:</p>



<ol class="wp-block-list">
<li>exact failing evidence from the CI log</li>



<li>likely root cause</li>



<li>smallest YAML-only fix, if any</li>



<li>exact YAML lines to change</li>



<li>whether I should validate locally or push directly</li>
</ol>



<p class="wp-block-paragraph">Then stop. Do not edit anything.</p>
</blockquote>



<h3 class="wp-block-heading">2. Minimal implementation prompt</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Apply this exact minimal fix.</p>



<p class="wp-block-paragraph">Do not diagnose.<br />Do not use agents.<br />Do not use read_agent.<br />Do not use SQL.<br />Do not use todo tracking.<br />Do not edit CI YAML.<br />Do not modify any files except:</p>



<ul class="wp-block-list">
<li>src\ElBruno.NetAgent\Services\Configuration\ConfigurationService.cs</li>



<li>tests\ElBruno.NetAgent.Tests\SafetyGuardrailTests.cs</li>
</ul>



<p class="wp-block-paragraph">If any write/edit tool fails with &#8220;Permission denied and could not request permission from user&#8221;, stop immediately. Do not try alternate write methods.</p>



<p class="wp-block-paragraph">Required change:<br />Add a test-only configuration directory override while preserving production default behavior.</p>



<p class="wp-block-paragraph">Validation:<br />Run only:<br />dotnet test .\tests\ElBruno.NetAgent.Tests\ElBruno.NetAgent.Tests.csproj &#8211;configuration Release &#8211;verbosity minimal</p>



<p class="wp-block-paragraph">After validation, summarize:</p>



<ul class="wp-block-list">
<li>exact files changed</li>



<li>test result</li>



<li>whether I should commit and push</li>
</ul>



<p class="wp-block-paragraph">Stop after summary.</p>
</blockquote>



<h3 class="wp-block-heading">3. Release readiness prompt</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">We are continuing the ElBruno.NetAgent project.</p>



<p class="wp-block-paragraph">Current repo:<br />C:\src\ElBruno.NetAgent</p>



<p class="wp-block-paragraph">Current status:</p>



<ul class="wp-block-list">
<li>Build passes.</li>



<li>Tests pass.</li>



<li>Pack passes.</li>



<li>Local package smoke test passes.</li>



<li>CI packaging validation passes.</li>



<li>No NuGet publishing has been performed.</li>



<li>The app remains dry-run only.</li>



<li>Live mode is blocked by default.</li>



<li>No real network mutation must be executed.</li>
</ul>



<p class="wp-block-paragraph">Important constraints:</p>



<ul class="wp-block-list">
<li>Do not use SQUAD.</li>



<li>Do not use task agents.</li>



<li>Do not use read_agent.</li>



<li>Do not use SQL or todo tracking.</li>



<li>Do not publish to NuGet.</li>



<li>Do not execute the app in a way that mutates network state.</li>



<li>Do not change dry-run safety behavior.</li>



<li>Do not add live network mutation.</li>



<li>Keep the phase bounded.</li>
</ul>



<p class="wp-block-paragraph">Implement Release Candidate Readiness, No Publish.</p>



<p class="wp-block-paragraph">Requirements:</p>



<ol class="wp-block-list">
<li>Review release readiness docs.</li>



<li>Add or update docs\RELEASE_CHECKLIST.md.</li>



<li>Confirm package metadata.</li>



<li>Confirm safety warnings are documented.</li>



<li>Confirm publishing remains manual.</li>



<li>Do not add NuGet publishing workflow.</li>
</ol>



<p class="wp-block-paragraph">Validation:<br />Run only:<br />dotnet build .\ElBruno.NetAgent.sln &#8211;configuration Release &#8211;no-restore<br />dotnet test .\ElBruno.NetAgent.sln &#8211;configuration Release &#8211;no-build &#8211;verbosity minimal<br />dotnet pack .\src\ElBruno.NetAgent\ElBruno.NetAgent.csproj &#8211;configuration Release &#8211;no-build &#8211;output .\artifacts<br />powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke-test-local-package.ps1 -PackagePath .\artifacts\ElBruno.NetAgent.0.1.0.nupkg</p>



<p class="wp-block-paragraph">After validation, summarize:</p>



<ul class="wp-block-list">
<li>files changed</li>



<li>build result</li>



<li>test result</li>



<li>pack result</li>



<li>smoke test result</li>



<li>whether this is ready for manual release review</li>
</ul>



<p class="wp-block-paragraph">Stop after summary.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">What did not work perfectly</h2>



<p class="wp-block-paragraph">This is the part that makes the experiment honest.</p>



<p class="wp-block-paragraph">Several things did not work smoothly.</p>



<h3 class="wp-block-heading">Runaway generation</h3>



<p class="wp-block-paragraph">Sometimes LM Studio kept generating tokens without returning a useful answer.</p>



<p class="wp-block-paragraph">The fix was usually:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">lower output token limit<br />reduce context<br />restart Copilot CLI<br />restart LM Studio server if needed<br />make the prompt smaller<br />add a clear stop condition</p>
</blockquote>



<h3 class="wp-block-heading">read_agent despite instructions</h3>



<p class="wp-block-paragraph">Even when the prompt said:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Do not use read_agent.</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Copilot CLI sometimes attempted it anyway.</p>



<p class="wp-block-paragraph">When that happened, I stopped the run.</p>



<h3 class="wp-block-heading">SQL/todo tracking attempts</h3>



<p class="wp-block-paragraph">Copilot CLI sometimes tried to use SQL/todo tracking even when I explicitly said not to.</p>



<p class="wp-block-paragraph">This often failed because the tool call was malformed, and it burned time and tokens.</p>



<h3 class="wp-block-heading">Tool permission issues</h3>



<p class="wp-block-paragraph">Before&nbsp;<code>/allow-all on</code>, write tools were blocked.</p>



<p class="wp-block-paragraph">After&nbsp;<code>/allow-all on</code>, writing worked, but I had to watch the run closely.</p>



<h3 class="wp-block-heading">Long validation loops</h3>



<p class="wp-block-paragraph">When I asked Copilot to validate everything, it sometimes got stuck in long shell/output loops.</p>



<p class="wp-block-paragraph">A better pattern was:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Copilot edits.<br />Copilot runs one targeted validation.<br />Human runs full validation manually.</p>
</blockquote>



<h3 class="wp-block-heading">Bigger context sometimes made things worse</h3>



<p class="wp-block-paragraph">Large context helped for docs and synthesis.</p>



<p class="wp-block-paragraph">But for CI diagnosis, smaller context was better.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Final result</h2>



<p class="wp-block-paragraph">By the end of this experiment, the project reached a solid validation state:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Build: passed<br />Tests: 125/125 passed<br />Pack: passed<br />Smoke test: passed<br />CI: passed<br />NuGet publish: not performed<br />Network mutation: not performed<br />Dry-run: preserved</p>
</blockquote>



<p class="wp-block-paragraph">The GitHub Actions workflow validates:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">restore<br />build<br />test<br />pack<br />local package smoke test<br />artifact upload</p>
</blockquote>



<p class="wp-block-paragraph">The app remains safe:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">dry-run by default<br />live mode blocked<br />no real network mutation</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">What I learned</h2>



<p class="wp-block-paragraph">The GPU did not make the local model magical.</p>



<p class="wp-block-paragraph">But it did change the workflow from:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">This is technically possible.</p>
</blockquote>



<p class="wp-block-paragraph">to:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">This can be useful if you control the scope.</p>
</blockquote>



<p class="wp-block-paragraph">The winning formula was:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">GitHub Copilot CLI</p>



<ul class="wp-block-list">
<li>LM Studio</li>



<li>Qwen local</li>



<li>GPU</li>



<li>small phases</li>



<li>strict prompts</li>



<li>build/test validation</li>



<li>human checkpoints</li>
</ul>
</blockquote>



<p class="wp-block-paragraph">The dangerous formula was:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">huge prompt</p>



<ul class="wp-block-list">
<li>huge context</li>



<li>full autonomy</li>



<li>unclear stop condition</li>



<li>long validation loops</li>
</ul>
</blockquote>



<p class="wp-block-paragraph">The best use case was not full autonomous coding.</p>



<p class="wp-block-paragraph">The best use case was:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">tool-assisted development<br />bounded implementation<br />targeted diagnosis<br />human-controlled validation</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">My final take</h2>



<p class="wp-block-paragraph">If I needed to ship this as fast as possible, I would use a powerful cloud model.</p>



<p class="wp-block-paragraph">No debate.</p>



<p class="wp-block-paragraph">A cloud model would likely be faster, more stable, and easier to orchestrate.</p>



<p class="wp-block-paragraph">But that was not the point of this experiment.</p>



<p class="wp-block-paragraph">The point was to understand whether a local model could participate in a real developer workflow through GitHub Copilot CLI.</p>



<p class="wp-block-paragraph">And the answer is:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Yes, but with constraints.</p>
</blockquote>



<p class="wp-block-paragraph">Local models are not yet the easiest path.</p>



<p class="wp-block-paragraph">They are not always the fastest path.</p>



<p class="wp-block-paragraph">But they are becoming good enough for serious experimentation, especially when you care about control, offline capability, privacy, and understanding how agentic coding behaves under real constraints.</p>



<p class="wp-block-paragraph">The human remains the control plane.</p>



<p class="wp-block-paragraph">And honestly, that is probably a good thing.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Cloud models are still faster and more convenient.<br />Local models are becoming good enough to be dangerous — in the best possible way.</p>
</blockquote>



<h2 class="wp-block-heading">Resources</h2>



<p class="wp-block-paragraph">Here are the main resources, tools, and references mentioned in this experiment.</p>



<ul class="wp-block-list">
<li><strong>ElBruno.NetAgent repository</strong><br /><a href="https://github.com/brunoghcpft-oss/ElBruno.NetAgent">https://github.com/brunoghcpft-oss/ElBruno.NetAgent</a></li>



<li><strong>Previous CPU-only experiment</strong><br /><a href="https://elbruno.com/2026/05/03/running-github-copilot-cli-offline-with-local-models-a-cpu-only-reality-check/">https://elbruno.com/2026/05/03/running-github-copilot-cli-offline-with-local-models-a-cpu-only-reality-check/</a></li>



<li><strong>GitHub Copilot CLI documentation</strong><br /><a href="https://docs.github.com/en/copilot/how-tos/copilot-cli">https://docs.github.com/en/copilot/how-tos/copilot-cli</a></li>



<li><strong>Using GitHub Copilot CLI</strong><br /><a href="https://docs.github.com/copilot/how-tos/use-copilot-agents/use-copilot-cli">https://docs.github.com/copilot/how-tos/use-copilot-agents/use-copilot-cli</a></li>



<li><strong>GitHub Copilot CLI product page</strong><br /><a href="https://github.com/features/copilot/cli">https://github.com/features/copilot/cli</a></li>



<li><strong>LM Studio</strong><br /><a href="https://lmstudio.ai/">https://lmstudio.ai/</a></li>



<li><strong>LM Studio GitHub organization</strong><br /><a href="https://github.com/lmstudio-ai">https://github.com/lmstudio-ai</a></li>



<li><strong>Qwen3.6-35B-A3B model on Hugging Face</strong><br /><a href="https://huggingface.co/Qwen/Qwen3.6-35B-A3B">https://huggingface.co/Qwen/Qwen3.6-35B-A3B</a></li>



<li><strong>LM Studio community GGUF for Qwen3.6-35B-A3B</strong><br /><a href="https://huggingface.co/lmstudio-community/Qwen3.6-35B-A3B-GGUF">https://huggingface.co/lmstudio-community/Qwen3.6-35B-A3B-GGUF</a></li>



<li><strong>Qwen3.6-35B-A3B announcement</strong><br /><a href="https://qwen.ai/blog?id=qwen3.6-35b-a3b">https://qwen.ai/blog?id=qwen3.6-35b-a3b</a></li>



<li><strong>Qwen3.6 GitHub repository</strong><br /><a href="https://github.com/QwenLM/Qwen3.6">https://github.com/QwenLM/Qwen3.6</a></li>



<li><strong>.NET downloads</strong><br /><a href="https://dotnet.microsoft.com/en-us/download">https://dotnet.microsoft.com/en-us/download</a></li>



<li><strong>GitHub CLI</strong><br /><a href="https://cli.github.com/">https://cli.github.com/</a></li>



<li><strong>GitHub Actions documentation</strong><br /><a href="https://docs.github.com/en/actions">https://docs.github.com/en/actions</a></li>



<li><strong>NuGet package: ElBruno.Text2Image.Cli</strong><br /><a href="https://www.nuget.org/packages/ElBruno.Text2Image.Cli">https://www.nuget.org/packages/ElBruno.Text2Image.Cli</a></li>



<li><strong>NuGet documentation</strong><br /><a href="https://learn.microsoft.com/en-us/nuget/">https://learn.microsoft.com/en-us/nuget/</a></li>



<li><strong>xUnit.net</strong><br /><a href="https://xunit.net/">https://xunit.net/</a></li>
</ul>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/05/06/running-github-copilot-cli-offline-with-local-models-gpu-edition/feed/</wfw:commentRss>
			<slash:comments>6</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40289</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-5-2026-04_09_28-pm-small.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/chatgpt-image-may-5-2026-04_09_28-pm-small.png">
			<media:title type="html">ChatGPT Image May 5, 2026, 04_09_28 PM-small</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/image.png?w=1024"/>
	</item>
		<item>
		<title>Running GitHub Copilot CLI Offline with Local Models: A CPU-Only Reality Check</title>
		<link>https://elbruno.com/2026/05/03/running-github-copilot-cli-offline-with-local-models-a-cpu-only-reality-check/</link>
					<comments>https://elbruno.com/2026/05/03/running-github-copilot-cli-offline-with-local-models-a-cpu-only-reality-check/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Sun, 03 May 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[GitHub Copilot CLI]]></category>
		<category><![CDATA[VM]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40267</guid>

					<description><![CDATA[TL;DR I tested GitHub Copilot CLI running offline with local models on a CPU-only Microsoft Dev Box. The short version: Ollama worked for basic offline prompts, but struggled with long-context agentic workflows and tool execution on CPU-only hardware. LM Studio worked better as the local OpenAI-compatible provider. google/gemma-4-e2b successfully executed Copilot CLI PowerShell tools. 32K [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading"></h2>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="571" src="https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_wduyfswduyfswduy.png?w=1024" alt="" class="wp-image-40283" srcset="https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_wduyfswduyfswduy.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_wduyfswduyfswduy.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_wduyfswduyfswduy.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_wduyfswduyfswduy.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_wduyfswduyfswduy.png 1376w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<h2 class="wp-block-heading">TL;DR</h2>



<p class="wp-block-paragraph">I tested <strong>GitHub Copilot CLI running offline with local models</strong> on a <strong>CPU-only Microsoft Dev Box</strong>.</p>



<p class="wp-block-paragraph">The short version:</p>



<ul class="wp-block-list">
<li><strong>Ollama worked</strong> for basic offline prompts, but struggled with long-context agentic workflows and tool execution on CPU-only hardware.</li>



<li><strong>LM Studio worked better</strong> as the local OpenAI-compatible provider.</li>



<li><code>google/gemma-4-e2b</code> successfully executed Copilot CLI PowerShell tools.</li>



<li><strong>32K and 64K context</strong> were the practical sweet spot for guided development.</li>



<li><strong>131K context worked</strong> as a stress test, but it was not practical as a daily CPU-only workflow.</li>



<li><strong>Copilot CLI interactive mode worked well</strong> with small, explicit, validated tasks.</li>



<li><strong>SQUAD/autopilot long workflows were not reliable enough</strong> on CPU-only hardware.</li>



<li>Final takeaway: local Copilot CLI works on CPU-only, but as a guided pair-programming workflow, not as a fully autonomous engineering team.</li>
</ul>



<p class="wp-block-paragraph">Or, in one sentence:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">CPU-only can be your copilot. Just do not promote it to engineering manager yet <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The experiment goal</h2>



<p class="wp-block-paragraph">This experiment started with a simple question:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Can I use GitHub Copilot CLI with a local model, offline, to work on a real .NET project?</p>
</blockquote>



<p class="wp-block-paragraph">The project was:</p>



<p class="wp-block-paragraph"><a href="https://github.com/brunoghcpft-oss/ElBruno.NetAgent">https://github.com/brunoghcpft-oss/ElBruno.NetAgent</a></p>



<p class="wp-block-paragraph">The bigger idea was also very attractive:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">GitHub Copilot CLI<br />+ SQUAD<br />+ local models<br />+ GitHub Copilot Free Tier<br />+ .NET<br />+ offline/local execution</p>
</blockquote>



<p class="wp-block-paragraph">That combination matters because it hints at a future where developers can experiment with local agentic workflows without always depending on cloud-hosted inference.</p>



<p class="wp-block-paragraph">But I did not want to only ask the model questions.</p>



<p class="wp-block-paragraph">I wanted to know if it could perform real developer tasks:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">dotnet new sln<br />dotnet new wpf<br />dotnet new xunit<br />dotnet build<br />dotnet test<br />git status<br />create files<br />update docs<br />work inside Copilot CLI interactively<br />use tools</p>
</blockquote>



<p class="wp-block-paragraph">That was the real test.</p>



<p class="wp-block-paragraph">Not “Can the model answer?”</p>



<p class="wp-block-paragraph">But:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Can the model act through Copilot CLI tools and help build a real .NET project?</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">First attempt: Ollama</h2>



<p class="wp-block-paragraph">The first local provider I tested was <strong>Ollama</strong>.</p>



<p class="wp-block-paragraph">Ollama is a great tool for running local models. It is simple, developer-friendly, and works very well for many local AI experiments.</p>



<p class="wp-block-paragraph">The initial Copilot CLI setup looked like this:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_BASE_URL</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;http://localhost:11434/v1&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_TYPE</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;openai&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_MODEL</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;qwen3.5&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_OFFLINE</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;true&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">I tried several models, including:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">qwen3.5<br />qwen3.5:4b<br />llama3.2<br />devstral-small-2<br />gemma4:e2b<br />qwen2.5-coder</p>
</blockquote>



<p class="wp-block-paragraph">The first big surprise was how large a “simple” Copilot CLI prompt actually becomes.</p>



<p class="wp-block-paragraph">Even something like:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Reply exactly: OK</p>
</blockquote>



<p class="wp-block-paragraph">is not just that text. Copilot CLI also includes system instructions, tool definitions, environment data, agent instructions, and other context.</p>



<p class="wp-block-paragraph">So a tiny user prompt can quickly become a large request.</p>



<p class="wp-block-paragraph">With Ollama on this CPU-only machine, I saw a consistent pattern:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Small context → truncation or failure<br />Larger context → very slow prompt processing<br />Tool execution → unreliable<br />CPU-only → retries, transient errors, long waits</p>
</blockquote>



<p class="wp-block-paragraph">Ollama did work in some scenarios. It responded to simple prompts and could be useful for ask-style interactions.</p>



<p class="wp-block-paragraph">But for Copilot CLI tool execution and agentic workflows, it was not reliable enough on this hardware.</p>



<p class="wp-block-paragraph">My conclusion for Ollama:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Ollama worked, but mostly for ask mode. In this CPU-only setup, it was not a good fit for full Copilot CLI agentic coding.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The context problem</h2>



<p class="wp-block-paragraph">One of the most important lessons was about <strong>context size</strong>.</p>



<p class="wp-block-paragraph">Copilot CLI does not only need room for your prompt. It also needs context for:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">system instructions<br />tool definitions<br />agent instructions<br />workspace information<br />command outputs<br />conversation history<br />MCP/tool metadata<br />custom agent context</p>
</blockquote>



<p class="wp-block-paragraph">The GitHub Copilot CLI best practices mention the value of models with large context windows for agentic work:</p>



<p class="wp-block-paragraph"><a href="https://docs.github.com/en/copilot/how-tos/copilot-cli/cli-best-practices">https://docs.github.com/en/copilot/how-tos/copilot-cli/cli-best-practices</a></p>



<p class="wp-block-paragraph">The command reference is also useful when exploring Copilot CLI behavior and options:</p>



<p class="wp-block-paragraph"><a href="https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference">https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference</a></p>



<p class="wp-block-paragraph">A task that looks small to us can become a much larger request for the CLI.</p>



<p class="wp-block-paragraph">For example:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Create a WPF project and run tests.</p>
</blockquote>



<p class="wp-block-paragraph">That sounds simple. But an agentic CLI workflow may need to include repo state, tools, instructions, command outputs, and validation steps.</p>



<p class="wp-block-paragraph">More context helps because the request fits better.</p>



<p class="wp-block-paragraph">But on CPU-only hardware, more context is not free:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">more context<br />= more prompt prefill<br />= more CPU work<br />= more waiting<br />= more chances for retries or timeouts</p>
</blockquote>



<p class="wp-block-paragraph">So context solved one problem and created another.</p>



<p class="wp-block-paragraph">The request could fit.</p>



<p class="wp-block-paragraph">But now the CPU had to process it.</p>



<p class="wp-block-paragraph">Slowly.</p>



<p class="wp-block-paragraph">Very slowly.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Moving from Ollama to LM Studio</h2>



<p class="wp-block-paragraph">The experiment changed when I moved from Ollama to <strong>LM Studio</strong>.</p>



<p class="wp-block-paragraph">LM Studio provides a local OpenAI-compatible server. In this setup, the endpoint was:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">http://localhost:1234/v1</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The Copilot CLI environment variables became:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_BASE_URL</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;http://localhost:1234/v1&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_TYPE</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;openai&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_MODEL</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;google/gemma-4-e2b&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_OFFLINE</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;true&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The model I used for the successful runs was:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">google/gemma-4-e2b</p>
</blockquote>



<p class="wp-block-paragraph">One LM Studio setting was critical:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Max Concurrent Predictions = 1</p>
</blockquote>



<p class="wp-block-paragraph">This matters because if the context length is set high but the server splits capacity across multiple concurrent predictions, each request may effectively get less usable context.</p>



<p class="wp-block-paragraph">For this experiment, I wanted one Copilot CLI request to get the full available context.</p>



<p class="wp-block-paragraph">The key LM Studio settings became:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Context Length: 32768, 65536, and 131072 during stress testing<br />Max Concurrent Predictions: 1<br />Keep Model in Memory: ON</p>
</blockquote>



<p class="wp-block-paragraph">LM Studio also gave me much better observability.</p>



<p class="wp-block-paragraph">With Ollama, I was mostly reading logs and guessing.</p>



<p class="wp-block-paragraph">With LM Studio, I could see the loaded model, server status, effective configuration, and live prompt processing progress.</p>



<p class="wp-block-paragraph">That mattered a lot.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">With LM Studio, I stopped guessing. I could see the prompt processing percentage moving.</p>
</blockquote>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="537" src="https://elbruno.com/wp-content/uploads/2026/05/ghcp-10-lmstudio-gemma4-128k-context-auto-pilot-run.png?w=1024" alt="" class="wp-image-40286" srcset="https://elbruno.com/wp-content/uploads/2026/05/ghcp-10-lmstudio-gemma4-128k-context-auto-pilot-run.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/05/ghcp-10-lmstudio-gemma4-128k-context-auto-pilot-run.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/05/ghcp-10-lmstudio-gemma4-128k-context-auto-pilot-run.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/05/ghcp-10-lmstudio-gemma4-128k-context-auto-pilot-run.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/05/ghcp-10-lmstudio-gemma4-128k-context-auto-pilot-run.png?w=1440 1440w, https://elbruno.com/wp-content/uploads/2026/05/ghcp-10-lmstudio-gemma4-128k-context-auto-pilot-run.png 1890w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The first real tool execution</h2>



<p class="wp-block-paragraph">The first major breakthrough was not the model answering a question.</p>



<p class="wp-block-paragraph">The breakthrough was tool execution.</p>



<p class="wp-block-paragraph">I asked Copilot CLI to use the PowerShell tool and create a file.</p>



<p class="wp-block-paragraph">The first attempt almost worked, but failed because the tool call was missing a required field:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">&#8220;description&#8221;: Required</p>
</blockquote>



<p class="wp-block-paragraph">That was an important lesson.</p>



<p class="wp-block-paragraph">The model was trying to use the tool, but the function call was incomplete.</p>



<p class="wp-block-paragraph">So I changed the prompt pattern.</p>



<p class="wp-block-paragraph">The working pattern became:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Use the powershell tool. The powershell tool call must include both required fields: command and description. Run this exact command: . Use this description: . Do not only describe the action.</p>
</blockquote>



<p class="wp-block-paragraph">Example:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Use the powershell tool. The powershell tool call must include both required fields: command and description. Run this exact command: echo LMSTUDIO_E2B_TOOL_TEST &gt; lmstudio-e2b-tool-test.txt. Use this description: Create LM Studio tool test file. Do not only describe the action.</p>
</blockquote>



<p class="wp-block-paragraph">This time, Copilot CLI executed the command.</p>



<p class="wp-block-paragraph">The file was created.</p>



<p class="wp-block-paragraph">That was the first real “OK”.</p>



<p class="wp-block-paragraph">Not just:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">The model answered.</p>
</blockquote>



<p class="wp-block-paragraph">But:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">The model used Copilot CLI tools to change the filesystem.</p>
</blockquote>



<p class="wp-block-paragraph">That changed the experiment.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">From 16K to 32K</h2>



<p class="wp-block-paragraph">At 16K context, basic tool execution worked.</p>



<p class="wp-block-paragraph">But when I tried to create a larger Markdown file using multiline content, the result became unreliable. The model claimed success, but the file was not created correctly.</p>



<p class="wp-block-paragraph">This taught me something important:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">More context is not the only problem. Command complexity matters too.</p>
</blockquote>



<p class="wp-block-paragraph">Long multiline here-strings, large embedded Markdown blocks, and complex PowerShell escaping were too much for this local setup.</p>



<p class="wp-block-paragraph">So I moved to a stricter execution rule:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">one prompt<br />one tool<br />one bounded command<br />one validation</p>
</blockquote>



<p class="wp-block-paragraph">Then I increased the context to 32K and started running real Phase 0-style .NET setup tasks.</p>



<p class="wp-block-paragraph">This worked.</p>



<p class="wp-block-paragraph">Copilot CLI successfully executed commands like:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">new</span> <span class="tok-variableName">sln</span> <span class="tok-operator">-</span><span class="tok-variableName">n</span> <span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">New-Item</span> <span class="tok-operator">-</span><span class="tok-variableName">ItemType</span> <span class="tok-variableName">Directory</span> <span class="tok-operator">-</span><span class="tok-variableName">Force</span> <span class="tok-operator">-</span><span class="tok-variableName">Path</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">src</span></div><div class="cm-line"><span class="tok-variableName">New-Item</span> <span class="tok-operator">-</span><span class="tok-variableName">ItemType</span> <span class="tok-variableName">Directory</span> <span class="tok-operator">-</span><span class="tok-variableName">Force</span> <span class="tok-operator">-</span><span class="tok-variableName">Path</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">tests</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">new</span> <span class="tok-variableName">wpf</span> <span class="tok-operator">-</span><span class="tok-variableName">n</span> <span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span> <span class="tok-operator">-</span><span class="tok-variableName">o</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">src</span><span class="tok-punctuation">\</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">new</span> <span class="tok-variableName">xunit</span> <span class="tok-operator">-</span><span class="tok-variableName">n</span> <span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span><span class="tok-punctuation">.</span><span class="tok-variableName">Tests</span> <span class="tok-operator">-</span><span class="tok-variableName">o</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">tests</span><span class="tok-punctuation">\</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span><span class="tok-punctuation">.</span><span class="tok-variableName">Tests</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">sln</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span><span class="tok-punctuation">.</span><span class="tok-variableName">sln</span> <span class="tok-variableName">add</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">src</span><span class="tok-punctuation">\</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span><span class="tok-punctuation">\</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span><span class="tok-punctuation">.</span><span class="tok-variableName">csproj</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">sln</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span><span class="tok-punctuation">.</span><span class="tok-variableName">sln</span> <span class="tok-variableName">add</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">tests</span><span class="tok-punctuation">\</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span><span class="tok-punctuation">.</span><span class="tok-variableName">Tests</span><span class="tok-punctuation">\</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span><span class="tok-punctuation">.</span><span class="tok-variableName">Tests</span><span class="tok-punctuation">.</span><span class="tok-variableName">csproj</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">And finally:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">build</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span><span class="tok-punctuation">.</span><span class="tok-variableName">sln</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">test</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span><span class="tok-punctuation">.</span><span class="tok-variableName">sln</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">This was a big milestone.</p>



<p class="wp-block-paragraph">With LM Studio, Gemma 4 E2B, and explicit tool instructions, Copilot CLI created a real .NET solution and validated it.</p>



<p class="wp-block-paragraph">The runtime was not fast. Most commands took several minutes.</p>



<p class="wp-block-paragraph">But they worked.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The practical prompt pattern</h2>



<p class="wp-block-paragraph">The reliable prompt style was very explicit.</p>



<p class="wp-block-paragraph">For example:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Use the powershell tool. The powershell tool call must include both required fields: command and description.</p>



<p class="wp-block-paragraph">Run this exact command:<br />dotnet build .\ElBruno.NetAgent.sln</p>



<p class="wp-block-paragraph">Use this description:<br />Build ElBruno NetAgent solution.</p>



<p class="wp-block-paragraph">Do not only describe the action.</p>
</blockquote>



<p class="wp-block-paragraph">This is not elegant.</p>



<p class="wp-block-paragraph">But it was reliable.</p>



<p class="wp-block-paragraph">The important parts were:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">name the tool<br />state the required fields<br />give the exact command<br />give the description<br />tell it not to only describe the action<br />include validation when possible</p>
</blockquote>



<p class="wp-block-paragraph">A better workflow emerged:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">ask for one small task<br />execute one command<br />validate the result<br />continue</p>
</blockquote>



<p class="wp-block-paragraph">That was the first point where the experiment became useful.</p>



<p class="wp-block-paragraph">Not autonomous.</p>



<p class="wp-block-paragraph">But useful.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Moving to 64K context</h2>



<p class="wp-block-paragraph">After 32K worked, I tested 64K.</p>



<p class="wp-block-paragraph">The environment variables became:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_BASE_URL</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;http://localhost:1234/v1&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_TYPE</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;openai&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_MODEL</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;google/gemma-4-e2b&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_MAX_PROMPT_TOKENS</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;65536&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_MAX_OUTPUT_TOKENS</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;512&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_OFFLINE</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;true&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">In LM Studio:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Context Length: 65536<br />Max Concurrent Predictions: 1</p>
</blockquote>



<p class="wp-block-paragraph">I tested a combined command:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">git</span> <span class="tok-variableName">status</span> <span class="tok-operator">--</span><span class="tok-variableName">short</span><span class="tok-punctuation">;</span> <span class="tok-variableName">dotnet</span> <span class="tok-variableName">test</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span><span class="tok-punctuation">.</span><span class="tok-variableName">sln</span><span class="tok-punctuation">;</span> <span class="tok-variableName">git</span> <span class="tok-variableName">status</span> <span class="tok-operator">--</span><span class="tok-variableName">short</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">It worked.</p>



<p class="wp-block-paragraph">The important observation was that 64K did not magically transform the model.</p>



<p class="wp-block-paragraph">The effective token usage was still around the same range as several 32K tests.</p>



<p class="wp-block-paragraph">So 64K gave the workflow more room, but it did not remove the need for disciplined prompts.</p>



<p class="wp-block-paragraph">My conclusion:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">64K worked, but it did not magically make the model smarter. It just gave the workflow more breathing room.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Testing Copilot CLI interactive mode</h2>



<p class="wp-block-paragraph">Up to this point, many tests used:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">copilot</span> <span class="tok-operator">-</span><span class="tok-variableName">p</span> <span class="tok-string">&quot;&lt;prompt&gt;&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That was useful for deterministic experiments, but it was not the workflow I really cared about.</p>



<p class="wp-block-paragraph">The real workflow should happen inside Copilot CLI.</p>



<p class="wp-block-paragraph">So I launched Copilot CLI interactively:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">copilot</span> <span class="tok-punctuation">`</span></div><div class="cm-line"><span class="tok-operator">--</span><span class="tok-variableName">disable-builtin-mcps</span> <span class="tok-punctuation">`</span></div><div class="cm-line"><span class="tok-operator">--</span><span class="tok-variableName">no-remote</span> <span class="tok-punctuation">`</span></div><div class="cm-line"><span class="tok-operator">--</span><span class="tok-variableName">yolo</span> <span class="tok-punctuation">`</span></div><div class="cm-line"><span class="tok-operator">--</span><span class="tok-variableName">stream</span> <span class="tok-variableName">on</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then I gave instructions inside the CLI.</p>



<p class="wp-block-paragraph">This also worked.</p>



<p class="wp-block-paragraph">I tested:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">git status &#8211;short<br />dotnet test .\ElBruno.NetAgent.sln<br />create folders<br />validate files</p>
</blockquote>



<p class="wp-block-paragraph">The interactive workflow was more natural.</p>



<p class="wp-block-paragraph">But the same rule still applied:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">small steps<br />explicit tool call<br />validation<br />no giant task requests</p>
</blockquote>



<p class="wp-block-paragraph">At this point, the conclusion was:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Interactive Copilot CLI with a local model is usable on CPU-only hardware, as long as the work is guided and constrained.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The 131K context stress test</h2>



<p class="wp-block-paragraph">LM Studio showed that <code>google/gemma-4-e2b</code> could be configured with a context length of 131072 tokens.</p>



<p class="wp-block-paragraph">So I tried the big one.</p>



<p class="wp-block-paragraph">In LM Studio:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Context Length: 131072<br />Max Concurrent Predictions: 1</p>
</blockquote>



<p class="wp-block-paragraph">In PowerShell:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_MAX_PROMPT_TOKENS</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;131072&quot;</span></div><div class="cm-line"><span class="tok-variableName">$env:COPILOT_PROVIDER_MAX_OUTPUT_TOKENS</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;1024&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then I ran Copilot CLI interactively and enabled autopilot.</p>



<p class="wp-block-paragraph">The stress test asked Copilot CLI to:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">inspect git status<br />inspect solution contents<br />run build<br />run tests<br />create a status file<br />validate the status file<br />show final git status</p>
</blockquote>



<p class="wp-block-paragraph">This worked well enough.</p>



<p class="wp-block-paragraph">The model executed build and tests, created the status file, and I manually validated the result.</p>



<p class="wp-block-paragraph">The generated status file contained:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">provider: LM Studio<br />model: google/gemma-4-e2b<br />context: 131072<br />execution mode: Copilot CLI interactive autopilot</p>
</blockquote>



<p class="wp-block-paragraph">Then I corrected it with a micro-prompt to add the missing result summary.</p>



<p class="wp-block-paragraph">Manual validation passed:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">Test-Path</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">docs</span><span class="tok-punctuation">\</span><span class="tok-variableName">PHASE0_STRESS_TEST_RESULT</span><span class="tok-punctuation">.</span><span class="tok-variableName">md</span></div><div class="cm-line"><span class="tok-variableName">Get-Content</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">docs</span><span class="tok-punctuation">\</span><span class="tok-variableName">PHASE0_STRESS_TEST_RESULT</span><span class="tok-punctuation">.</span><span class="tok-variableName">md</span></div><div class="cm-line"><span class="tok-variableName">git</span> <span class="tok-variableName">status</span> <span class="tok-operator">--</span><span class="tok-variableName">short</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">test</span> <span class="tok-punctuation">.\</span><span class="tok-variableName">ElBruno</span><span class="tok-punctuation">.</span><span class="tok-variableName">NetAgent</span><span class="tok-punctuation">.</span><span class="tok-variableName">sln</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">So, yes:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">131K context worked.</p>
</blockquote>



<p class="wp-block-paragraph">But there is an important nuance:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">131K worked technically. That does not mean 131K was practical on CPU-only hardware.</p>
</blockquote>



<p class="wp-block-paragraph">For the successful stress test, Copilot CLI did not actually need to consume the full 131K context. The larger context was available, but the workflow still depended on bounded tasks.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The SQUAD test</h2>



<p class="wp-block-paragraph">Finally, I tried the big question:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Can SQUAD run this as a more autonomous workflow?</p>
</blockquote>



<p class="wp-block-paragraph">This was the most interesting failure.</p>



<p class="wp-block-paragraph">I reactivated the SQUAD custom agent and asked it to:</p>



<ol class="wp-block-list">
<li>Inspect the repository.</li>



<li>Update the existing SQUAD execution rules with a local model execution rule.</li>



<li>Implement a simple WPF status display.</li>



<li>Build.</li>



<li>Test.</li>



<li>Show final git status.</li>



<li>Summarize the changes.</li>
</ol>



<p class="wp-block-paragraph">This is where CPU-only started to show the real limit.</p>



<p class="wp-block-paragraph">Copilot CLI selected the custom agent:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Selected custom agent: Squad</p>
</blockquote>



<p class="wp-block-paragraph">Then transient API errors started appearing.</p>



<p class="wp-block-paragraph">Then a tool call failed because it was missing the required <code>description</code> field:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">&#8220;description&#8221;: Required</p>
</blockquote>



<p class="wp-block-paragraph">After that, it executed <code>git status --short</code>, but then started claiming that the workflow had completed, including rule updates, build/test sequence, and feature integration.</p>



<p class="wp-block-paragraph">The problem: the log did not show reliable tool-call evidence for all those claimed actions.</p>



<p class="wp-block-paragraph">That is the danger zone.</p>



<p class="wp-block-paragraph">The model can say:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">Done.</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">But as developers, we need:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Show me the tool output.<br />Show me the file diff.<br />Show me the build.<br />Show me the test result.</p>
</blockquote>



<p class="wp-block-paragraph">The LM Studio logs showed the other side of the problem: long SQUAD/autopilot workflows generate lots of reasoning and inflate the prompt. The model was processing a large conversation and spending a lot of time in reasoning rather than clean tool execution.</p>



<p class="wp-block-paragraph">This was the final conclusion for SQUAD on CPU-only:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">SQUAD/autopilot long workflows are not reliable enough on this hardware. The model can drift, retries can appear, malformed tool calls can happen, and it may claim completion without enough verified execution.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">What worked well</h2>



<p class="wp-block-paragraph">This setup worked well for guided work:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">LM Studio</p>



<ul class="wp-block-list">
<li>google/gemma-4-e2b</li>



<li>Copilot CLI</li>



<li>offline mode</li>



<li>PowerShell tools</li>



<li>32K / 64K context</li>



<li>small tasks</li>



<li>manual validation</li>
</ul>
</blockquote>



<p class="wp-block-paragraph">It successfully handled:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">dotnet new sln<br />dotnet new wpf<br />dotnet new xunit<br />dotnet sln add<br />dotnet build<br />dotnet test<br />git status<br />file creation<br />small docs updates<br />interactive Copilot CLI prompts<br />autopilot stress test with manual validation</p>
</blockquote>



<p class="wp-block-paragraph">That is a real result.</p>



<p class="wp-block-paragraph">It means local model workflows for .NET development are possible on CPU-only hardware.</p>



<p class="wp-block-paragraph">Slow, yes.</p>



<p class="wp-block-paragraph">But possible.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">What did not work well</h2>



<p class="wp-block-paragraph">This setup did not work well for:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">long autonomous SQUAD execution<br />large “do the whole phase” prompts<br />complex multiline file generation<br />large here-strings<br />unattended autopilot loops<br />claims without validation<br />CPU-only 131K workflows as a daily mode</p>
</blockquote>



<p class="wp-block-paragraph">The main risks were:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">very slow prompt processing<br />transient API errors<br />malformed tool calls<br />missing required tool fields<br />reasoning drift<br />false completion claims<br />too much context for CPU-only hardware</p>
</blockquote>



<p class="wp-block-paragraph">The most important failure mode was not:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">The model cannot answer.</p>
</blockquote>



<p class="wp-block-paragraph">The most important failure mode was:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">The model claims that work is complete, but the validated tool evidence is incomplete.</p>
</blockquote>



<p class="wp-block-paragraph">That is why manual validation is not optional.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">The sweet spot</h2>



<p class="wp-block-paragraph">For this CPU-only VM, the sweet spot is:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Provider: LM Studio<br />Model: google/gemma-4-e2b<br />Context: 32K or 64K<br />Max Concurrent Predictions: 1<br />Copilot CLI: offline + no remote<br />Mode: interactive<br />Workflow: one bounded task at a time<br />Validation: always</p>
</blockquote>



<p class="wp-block-paragraph">The best working rule:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">One prompt.<br />One tool.<br />One bounded command.<br />One validation.</p>
</blockquote>



<p class="wp-block-paragraph">This is not as flashy as “fully autonomous AI developer.”</p>



<p class="wp-block-paragraph">But it is much more realistic.</p>



<p class="wp-block-paragraph">And it works.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Recommended Copilot CLI setup</h2>



<p class="wp-block-paragraph">This is the setup I would keep for guided local work:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">$env:COPILOT_PROVIDER_BASE_URL = &quot;http://localhost:1234/v1&quot;</div><div class="cm-line">$env:COPILOT_PROVIDER_TYPE = &quot;openai&quot;</div><div class="cm-line">$env:COPILOT_MODEL = &quot;google/gemma-4-e2b&quot;</div><div class="cm-line">$env:COPILOT_PROVIDER_MAX_PROMPT_TOKENS = &quot;65536&quot;</div><div class="cm-line">$env:COPILOT_PROVIDER_MAX_OUTPUT_TOKENS = &quot;512&quot;</div><div class="cm-line">$env:COPILOT_OFFLINE = &quot;true&quot;</div><div class="cm-line"></div><div class="cm-line">copilot `</div><div class="cm-line">  --disable-builtin-mcps `</div><div class="cm-line">  --no-remote `</div><div class="cm-line">  --yolo `</div><div class="cm-line">  --stream on</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then inside Copilot CLI, use prompts like this:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Use the powershell tool. The powershell tool call must include both required fields: command and description.</p>



<p class="wp-block-paragraph">Run this exact command:<br />dotnet test .\ElBruno.NetAgent.sln</p>



<p class="wp-block-paragraph">Use this description:<br />Run ElBruno NetAgent tests.</p>



<p class="wp-block-paragraph">Do not only describe the action.</p>
</blockquote>



<p class="wp-block-paragraph">Again, not elegant.</p>



<p class="wp-block-paragraph">But reliable.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Test environment</h2>



<p class="wp-block-paragraph">These results are based on one CPU-only virtual machine. They should not be treated as a universal benchmark for Copilot CLI, LM Studio, Ollama, or Gemma 4 E2B.</p>



<p class="wp-block-paragraph">Hardware and OS:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Environment: Microsoft Dev Box<br />Dev Box size: AMD 8 vCPU / 32 GB RAM / 1024 GB storage<br />Processor: AMD EPYC 7763 64-Core Processor @ 2.44 GHz<br />Installed RAM: 32.0 GB<br />Storage: 1.00 TB<br />GPU: None<br />Dedicated VRAM: None<br />System type: 64-bit operating system, x64-based processor<br />Operating system: Windows 11 Enterprise<br />Windows version: 25H2<br />OS build: 26200.8246</p>
</blockquote>



<p class="wp-block-paragraph">Experiment setup:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Shell: PowerShell<br />Main successful local model host: LM Studio<br />Main successful model: google/gemma-4-e2b<br />Other local model host tested: Ollama<br />Copilot account: GitHub Copilot Free Tier<br />Copilot CLI mode: offline / local provider<br />Project type: .NET WPF app + xUnit test project</p>
</blockquote>



<p class="wp-block-paragraph">The important detail is that this machine had <strong>no dedicated GPU</strong> and <strong>no dedicated VRAM</strong>.</p>



<p class="wp-block-paragraph">All local model inference happened on CPU.</p>



<p class="wp-block-paragraph">That is why long-context workflows, especially SQUAD/autopilot runs, became slow and fragile even when the model technically supported 131K context.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Final conclusion</h2>



<p class="wp-block-paragraph">This experiment changed my expectations.</p>



<p class="wp-block-paragraph">At the beginning, I thought the question was:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Which local model works with Copilot CLI?</p>
</blockquote>



<p class="wp-block-paragraph">By the end, the real question was:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">What workflow makes local Copilot CLI reliable enough to be useful?</p>
</blockquote>



<p class="wp-block-paragraph">The answer was not just the model.</p>



<p class="wp-block-paragraph">The answer was the combination of:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">LM Studio</p>



<ul class="wp-block-list">
<li>Gemma 4 E2B</li>



<li>enough context</li>



<li>low concurrency</li>



<li>explicit tool instructions</li>



<li>small tasks</li>



<li>constant validation</li>
</ul>
</blockquote>



<p class="wp-block-paragraph">On CPU-only hardware, the result is clear:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Good for guided pair programming.<br />Good for small .NET tasks.<br />Good for local/offline experiments.<br />Good for learning how agentic workflows behave.</p>



<p class="wp-block-paragraph">Not good for fully autonomous SQUAD execution.<br />Not good for long unattended implementation phases.<br />Not good for pretending CPU-only is a cloud GPU cluster.</p>
</blockquote>



<p class="wp-block-paragraph">Local AI is real.</p>



<p class="wp-block-paragraph">Offline developer workflows are real.</p>



<p class="wp-block-paragraph">Copilot CLI with local models is real.</p>



<p class="wp-block-paragraph">But autonomy still needs the right model, the right hardware, and a lot of discipline.</p>



<p class="wp-block-paragraph">CPU-only can be your copilot.</p>



<p class="wp-block-paragraph">Just do not promote it to engineering manager yet <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/05/03/running-github-copilot-cli-offline-with-local-models-a-cpu-only-reality-check/feed/</wfw:commentRss>
			<slash:comments>10</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40267</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_wduyfswduyfswduy.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_wduyfswduyfswduy.png">
			<media:title type="html">Gemini_Generated_Image_wduyfswduyfswduy</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/gemini_generated_image_wduyfswduyfswduy.png?w=1024"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/05/ghcp-10-lmstudio-gemma4-128k-context-auto-pilot-run.png?w=1024"/>
	</item>
		<item>
		<title>OllamaMonitor — Monitor Your Local Ollama Runtime from the Windows Tray</title>
		<link>https://elbruno.com/2026/04/28/ollamamonitor-monitor-your-local-ollama-runtime-from-the-windows-tray/</link>
					<comments>https://elbruno.com/2026/04/28/ollamamonitor-monitor-your-local-ollama-runtime-from-the-windows-tray/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Tue, 28 Apr 2026 12:17:00 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[CLI]]></category>
		<category><![CDATA[CLI Tools]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[NuGet]]></category>
		<category><![CDATA[NuGet Packages]]></category>
		<category><![CDATA[Ollama]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40260</guid>

					<description><![CDATA[Hi! If you&#8217;re running Ollama locally on Windows—whether you&#8217;re tinkering with LLMs, building local AI demos, or just curious about the overhead of large models—you&#8217;ve probably wondered: Is it still running? How much CPU is it chewing? Did that model load? Welcome to&#160;ElBruno.OllamaMonitor. It&#8217;s a no-frills system tray app that sits in your Windows notification area [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<figure class="wp-block-image size-large"><img loading="lazy" width="426" height="240" src="https://elbruno.com/wp-content/uploads/2026/04/ollamanitor-demo01.gif?w=426" alt="" class="wp-image-40261" srcset="https://elbruno.com/wp-content/uploads/2026/04/ollamanitor-demo01.gif 426w, https://elbruno.com/wp-content/uploads/2026/04/ollamanitor-demo01.gif?w=150 150w, https://elbruno.com/wp-content/uploads/2026/04/ollamanitor-demo01.gif?w=300 300w" sizes="auto, (max-width: 426px) 100vw, 426px" /></figure>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">If you&#8217;re running Ollama locally on Windows—whether you&#8217;re tinkering with LLMs, building local AI demos, or just curious about the overhead of large models—you&#8217;ve probably wondered: <em>Is it still running? How much CPU is it chewing? Did that model load?</em></p>



<p class="wp-block-paragraph">Welcome to&nbsp;<strong>ElBruno.OllamaMonitor</strong>.</p>



<p class="wp-block-paragraph">It&#8217;s a no-frills system tray app that sits in your Windows notification area and tells you, at a glance, exactly what your local Ollama instance is doing. No dashboards. No complexity. Just real-time status, resource metrics, and a floating details window when you need more info.</p>



<p class="wp-block-paragraph">ElBruno.OllamaMonitor sits in your Windows system tray and tells you:</p>



<ul class="wp-block-list">
<li><strong>Is Ollama running?</strong> A glance at the tray icon shows you the status.</li>



<li><strong>Is a model loaded?</strong> See what’s currently active.</li>



<li><strong>How much CPU, RAM, and GPU is it using?</strong> Real-time resource metrics from the Ollama process.</li>



<li><strong>Any errors?</strong> Get instant visual feedback if something’s wrong.</li>
</ul>



<p class="wp-block-paragraph">Perfect for:</p>



<ul class="wp-block-list">
<li>Local AI developers who need quick visibility into Ollama</li>



<li>Demo presenters who want to know resource impact in real-time</li>



<li>Anyone running large models locally who’s curious about the overhead</li>
</ul>



<h2 class="wp-block-heading">Installation<a href="https://elbruno.github.io/ElBruno.OllamaMonitor/#installation"></a></h2>



<h3 class="wp-block-heading">Via NuGet (Recommended)<a href="https://elbruno.github.io/ElBruno.OllamaMonitor/#via-nuget-recommended"></a></h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet tool install --global ElBruno.OllamaMonitor</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then launch anytime:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">ollamamon</div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">From Source<a href="https://elbruno.github.io/ElBruno.OllamaMonitor/#from-source"></a></h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">git clone https://github.com/elbruno/ElBruno.OllamaMonitor.git</div><div class="cm-line">cd ElBruno.OllamaMonitor</div><div class="cm-line">dotnet build src/ElBruno.OllamaMonitor/</div><div class="cm-line">dotnet run --project src/ElBruno.OllamaMonitor/</div></code></pre>
		</div>
	</div>
</div>


<h2 class="wp-block-heading">Quick Start<a href="https://elbruno.github.io/ElBruno.OllamaMonitor/#quick-start"></a></h2>



<ol class="wp-block-list">
<li><strong>Launch the app:</strong><code>ollamamon </code>The app starts minimized to the tray. Click the icon to open the details window or mini monitor.</li>



<li><strong>Check your status:</strong> Look at the tray icon color—it tells you Ollama’s status at a glance.</li>



<li><strong>Configure (optional):</strong> See <a href="https://elbruno.github.io/ElBruno.OllamaMonitor/docs/configuration.html">Configuration Guide</a> for endpoint, refresh rate, and threshold settings.</li>
</ol>



<h2 class="wp-block-heading">Features<a href="https://elbruno.github.io/ElBruno.OllamaMonitor/#features"></a></h2>



<ul class="wp-block-list">
<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>System Tray Integration</strong> — Runs in the background, always visible</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Visual Status Indicators</strong> — Color-coded icons for quick status checks</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Standard Details Window</strong> — A normal Windows window with minimize/close behavior that keeps the app in the tray when closed</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Mini Monitor Window</strong> — A semi-transparent always-on-top compact view for CPU, RAM, GPU, and model status</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Local Configuration</strong> — Customize endpoint, refresh rate, thresholds</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>CLI Commands</strong> — Fully scriptable configuration</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>GPU Metrics</strong> — Best-effort NVIDIA GPU tracking (if nvidia-smi is available)</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Copy to Clipboard</strong> — Quickly share diagnostics</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Manual Refresh</strong> — Force an immediate check</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Open Ollama URL</strong> — Quick link to the Ollama API</li>
</ul>



<h2 class="wp-block-heading">Requirements<a href="https://elbruno.github.io/ElBruno.OllamaMonitor/#requirements"></a></h2>



<ul class="wp-block-list">
<li><strong>Windows 10 / Windows 11</strong> (requires .NET 10 runtime, which can be downloaded from <a href="https://dotnet.microsoft.com/">dotnet.microsoft.com</a>)</li>



<li><strong>Ollama</strong> running locally (download from <a href="https://ollama.ai/">ollama.ai</a>)</li>



<li><strong>.NET 10 SDK</strong> to build from source</li>
</ul>



<p class="wp-block-paragraph">Optional:</p>



<ul class="wp-block-list">
<li><strong>nvidia-smi</strong> (NVIDIA GPU drivers) for GPU metrics</li>
</ul>



<h2 class="wp-block-heading">Configuration<a href="https://elbruno.github.io/ElBruno.OllamaMonitor/#configuration"></a></h2>



<p class="wp-block-paragraph">See&nbsp;<a href="https://elbruno.github.io/ElBruno.OllamaMonitor/docs/configuration.html">Configuration Guide</a>&nbsp;for detailed setup, CLI commands, custom thresholds, and advanced options like remote Ollama monitoring.</p>



<p class="wp-block-paragraph"><strong>Try it:</strong>&nbsp;<code>dotnet tool install --global ElBruno.OllamaMonitor</code></p>



<p class="wp-block-paragraph"><strong>Docs:</strong>&nbsp;See the&nbsp;<a href="https://github.com/ElBruno/ElBruno.OllamaMonitor">GitHub repository</a></p>



<p class="wp-block-paragraph"><strong>Made by:</strong>&nbsp;<a href="https://elbruno.com/">El Bruno</a>&nbsp;— A .NET developer obsessed with local AI and productivity.</p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/04/28/ollamamonitor-monitor-your-local-ollama-runtime-from-the-windows-tray/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40260</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/04/ollamanitor-demo01.gif"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/ollamanitor-demo01.gif">
			<media:title type="html">ollamanitor-demo01</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/ollamanitor-demo01.gif?w=426"/>
	</item>
		<item>
		<title>AspireMonitor: Stop bouncing between your Aspire dashboard and your code.</title>
		<link>https://elbruno.com/2026/04/27/aspiremonitor-stop-bouncing-between-your-aspire-dashboard-and-your-code/</link>
					<comments>https://elbruno.com/2026/04/27/aspiremonitor-stop-bouncing-between-your-aspire-dashboard-and-your-code/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Mon, 27 Apr 2026 12:48:15 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[Aspire]]></category>
		<category><![CDATA[CLI Tools]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[NuGet]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40256</guid>

					<description><![CDATA[Hi! What it is AspireMonitor is a Windows tray app that puts your Aspire AppHost status one click away. No browser tabs. No switching context. Click the tray icon, see what&#8217;s running, Start/Stop your app, or pin the resources you actually care about in a compact mini window. Why it exists If you use Aspire, [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.AspireMonitor/blob/main/docs/promotional/blog-post.md#aspiremonitor-stop-bouncing-between-your-aspire-dashboard-and-your-code"></a></p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.AspireMonitor/blob/main/docs/promotional/screenshots/hero-banner.png" target="_blank" rel="noreferrer noopener"><img src="https://github.com/elbruno/ElBruno.AspireMonitor/raw/main/docs/promotional/screenshots/hero-banner.png" alt="AspireMonitor — your Aspire AppHost, one tray icon away" /></a></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
dotnet tool install --global ElBruno.AspireMonitor
aspiremon
</pre></div>


<h2 class="wp-block-heading">What it is<a href="https://github.com/elbruno/ElBruno.AspireMonitor/blob/main/docs/promotional/blog-post.md#what-it-is"></a></h2>



<p class="wp-block-paragraph">AspireMonitor is a Windows tray app that puts your Aspire AppHost status one click away. No browser tabs. No switching context. Click the tray icon, see what&#8217;s running, Start/Stop your app, or pin the resources you actually care about in a compact mini window.</p>



<h2 class="wp-block-heading">Why it exists<a href="https://github.com/elbruno/ElBruno.AspireMonitor/blob/main/docs/promotional/blog-post.md#why-it-exists"></a></h2>



<p class="wp-block-paragraph">If you use Aspire, you know the dashboard is useful—but you don&#8217;t want a browser tab open all day. You&#8217;re writing code, not monitoring. AspireMonitor solves the friction: a lightweight tray icon that gives you &#8220;is my API up?&#8221; and &#8220;what URL does this service run on?&#8221; without leaving your IDE.</p>



<h2 class="wp-block-heading">Why it&#8217;s useful<a href="https://github.com/elbruno/ElBruno.AspireMonitor/blob/main/docs/promotional/blog-post.md#why-its-useful"></a></h2>



<p class="wp-block-paragraph"><strong>v1.4.0 ships with two killer features:</strong></p>



<p class="wp-block-paragraph">The mini window pins only the resources you care about (configure once:&nbsp;<code>web, store, gateway</code>) with their live URLs and Start/Stop buttons. The main window shows the full resource list when you need it.</p>



<p class="wp-block-paragraph">Start now works correctly—it stays disabled with&nbsp;<code>&#x23f3; Starting Aspire... (12 / 90s)</code>&nbsp;so you know when resources are actually ready. Stop actually stops. URLs are real (<code>http://localhost:5021</code>), not generic &#8220;Open&#8221; links.</p>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.AspireMonitor/blob/main/docs/promotional/screenshots/mini-window.png" target="_blank" rel="noreferrer noopener"><img src="https://github.com/elbruno/ElBruno.AspireMonitor/raw/main/docs/promotional/screenshots/mini-window.png" alt="AspireMonitor mini window—pinned resources with live URLs" /></a></figure>



<figure class="wp-block-image"><a href="https://github.com/elbruno/ElBruno.AspireMonitor/blob/main/docs/promotional/screenshots/main-window.png" target="_blank" rel="noreferrer noopener"><img src="https://github.com/elbruno/ElBruno.AspireMonitor/raw/main/docs/promotional/screenshots/main-window.png" alt="AspireMonitor main window—full resource list" /></a></figure>



<h2 class="wp-block-heading">Get it<a href="https://github.com/elbruno/ElBruno.AspireMonitor/blob/main/docs/promotional/blog-post.md#get-it"></a></h2>



<ul class="wp-block-list">
<li><strong>NuGet:</strong> <a href="https://www.nuget.org/packages/ElBruno.AspireMonitor">https://www.nuget.org/packages/ElBruno.AspireMonitor</a></li>



<li><strong>GitHub:</strong> <a href="https://github.com/elbruno/ElBruno.AspireMonitor">https://github.com/elbruno/ElBruno.AspireMonitor</a></li>



<li><strong>MIT licensed.</strong> Issues and PRs welcome.</li>
</ul>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Built with .NET 10 + WPF. Shells out to&nbsp;<code>aspire describe</code>—no third-party SDK dependency.</p>
</blockquote>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/04/27/aspiremonitor-stop-bouncing-between-your-aspire-dashboard-and-your-code/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40256</post-id>
		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.AspireMonitor/raw/main/docs/promotional/screenshots/hero-banner.png">
			<media:title type="html">AspireMonitor — your Aspire AppHost, one tray icon away</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.AspireMonitor/raw/main/docs/promotional/screenshots/mini-window.png">
			<media:title type="html">AspireMonitor mini window—pinned resources with live URLs</media:title>
		</media:content>

		<media:content medium="image" url="https://github.com/elbruno/ElBruno.AspireMonitor/raw/main/docs/promotional/screenshots/main-window.png">
			<media:title type="html">AspireMonitor main window—full resource list</media:title>
		</media:content>
	</item>
		<item>
		<title>&#128347; ClockTray v1.0.0: Meet the CLI-Enabled Clock Tool for Windows</title>
		<link>https://elbruno.com/2026/04/23/%f0%9f%95%9b-clocktray-v1-0-0-meet-the-cli-enabled-clock-tool-for-windows/</link>
					<comments>https://elbruno.com/2026/04/23/%f0%9f%95%9b-clocktray-v1-0-0-meet-the-cli-enabled-clock-tool-for-windows/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Thu, 23 Apr 2026 11:01:33 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[CLI]]></category>
		<category><![CDATA[CLI Tools]]></category>
		<category><![CDATA[ClockTray]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[NuGet]]></category>
		<category><![CDATA[technology]]></category>
		<category><![CDATA[Windows]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40251</guid>

					<description><![CDATA[Hi! usually hide my taskbar clock while recording videos, and after doing that dance one too many times, I had one goal:&#160;save a few clicks. Then yesterday, during a GitHub Copilot CLI session (watch it here), I decided to pick up the ClockTray app and turn it into a CLI tool. And&#8230; here we are. [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" width="512" height="512" src="https://elbruno.com/wp-content/uploads/2026/04/clocktray-cli-small.png?w=512" alt="" class="wp-image-40253" srcset="https://elbruno.com/wp-content/uploads/2026/04/clocktray-cli-small.png 512w, https://elbruno.com/wp-content/uploads/2026/04/clocktray-cli-small.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/04/clocktray-cli-small.png?w=300 300w" sizes="auto, (max-width: 512px) 100vw, 512px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">usually hide my taskbar clock while recording videos, and after doing that dance one too many times, I had one goal:&nbsp;<strong>save a few clicks</strong>.</p>



<p class="wp-block-paragraph">Then yesterday, during a GitHub Copilot CLI session (<a href="https://www.youtube.com/watch?v=uVTgs4WieY4">watch it here</a>), I decided to pick up the ClockTray app and turn it into a CLI tool.</p>



<p class="wp-block-paragraph">And&#8230; here we are. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<h2 class="wp-block-heading">Introducing the ClockTray CLI </h2>



<p class="wp-block-paragraph">ClockTray v1.0.0 is now a <strong>dual-mode utility</strong> combining a user-friendly GUI with powerful command-line controls.</p>



<p class="wp-block-paragraph">The headline feature?&nbsp;<strong>Full CLI support.</strong>&nbsp;You can now control your taskbar clock entirely from PowerShell, batch scripts, or any automation tool. Show it, hide it, check its status—all without touching the mouse.</p>



<h2 class="wp-block-heading">The New CLI Commands</h2>



<p class="wp-block-paragraph">Here&#8217;s what you can do right from your terminal:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line"># Show the clock on your taskbar</div><div class="cm-line">clocktray show</div><div class="cm-line"></div><div class="cm-line"># Hide the clock</div><div class="cm-line">clocktray hide</div><div class="cm-line"></div><div class="cm-line"># Check the current status</div><div class="cm-line">clocktray status</div><div class="cm-line"></div><div class="cm-line"># View all available commands</div><div class="cm-line">clocktray --help</div><div class="cm-line"></div><div class="cm-line"># Check the version</div><div class="cm-line">clocktray --version</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Each command is instant, non-blocking, and integrates seamlessly with your scripts and workflows.</p>



<h2 class="wp-block-heading">One Tool, Two Modes</h2>



<p class="wp-block-paragraph">ClockTray now works the way&nbsp;<em>you</em>&nbsp;work:</p>



<p class="wp-block-paragraph"><strong>GUI Mode:</strong>&nbsp;Launch the application normally for a traditional system tray experience. Right-click the icon to toggle your clock or access settings—perfect for quick manual control.</p>



<p class="wp-block-paragraph"><strong>CLI Mode:</strong>&nbsp;Run&nbsp;<code>clocktray</code>&nbsp;commands from PowerShell, Windows Terminal, or your build scripts. Perfect for automation, CI/CD pipelines, and repetitive tasks.</p>



<p class="wp-block-paragraph">You don&#8217;t need separate tools. You get both, in a single 30-second install.</p>



<h2 class="wp-block-heading">Automation &amp; Real-World Use Cases</h2>



<p class="wp-block-paragraph">Imagine these scenarios—now all possible with ClockTray:</p>



<ul class="wp-block-list">
<li><strong>Streaming Setup:</strong> Hide the clock before you go live, show it again when you stop.</li>



<li><strong>Multi-Monitor Management:</strong> Toggle the clock based on which display you&#8217;re using—via a custom PowerShell profile.</li>



<li><strong>Build Pipelines:</strong> Hide the clock during automated testing, restore it when complete.</li>



<li><strong>Accessibility Workflows:</strong> Create quick-access scripts that adapt your desktop to different needs.</li>



<li><strong>Time-Tracking Scripts:</strong> Check the clock status as part of a larger automation routine.</li>
</ul>



<p class="wp-block-paragraph">PowerShell scripters and DevOps engineers, you&#8217;re going to love this.</p>



<h2 class="wp-block-heading">Bonus Feature: Chinese Lunar Calendar Overlay</h2>



<p class="wp-block-paragraph">ClockTray also includes a beautiful overlay showing the Chinese lunar calendar—perfect if you&#8217;re tracking traditional holidays, working with international teams, or simply interested in lunar dates. This overlay displays alongside your taskbar clock and respects your show/hide commands.</p>



<h2 class="wp-block-heading">Installation in 30 Seconds</h2>



<p class="wp-block-paragraph">You can install ClockTray as a global .NET tool from NuGet:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet tool install --global ElBruno.ClockTray</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That&#8217;s it. You&#8217;ll have&nbsp;<code>clocktray</code>&nbsp;available in any PowerShell session or terminal immediately.</p>



<p class="wp-block-paragraph">Update an existing installation? Use:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet tool update --global ElBruno.ClockTray</div></code></pre>
		</div>
	</div>
</div>


<h2 class="wp-block-heading">Get ClockTray Today</h2>



<ul class="wp-block-list">
<li><strong>GitHub:</strong> <a href="https://github.com/elbruno/ElBruno.ClockTray">github.com/elbruno/ElBruno.ClockTray</a></li>



<li><strong>NuGet:</strong> <a href="https://www.nuget.org/packages/ElBruno.ClockTray">nuget.org/packages/ElBruno.ClockTray</a></li>
</ul>



<p class="wp-block-paragraph">Explore the source code, contribute features, or report issues on GitHub. The project is open-source and welcomes your feedback.</p>



<h2 class="wp-block-heading">What&#8217;s Next</h2>



<p class="wp-block-paragraph">With v1.0.0 released, we&#8217;re already thinking ahead. Future updates may include:</p>



<ul class="wp-block-list">
<li>Configuration files for default behavior</li>



<li>Extended automation scenarios</li>



<li>Cross-platform considerations</li>
</ul>



<p class="wp-block-paragraph">Your input shapes this tool. If you have ideas, open an issue or discussion on GitHub.</p>



<h2 class="wp-block-heading">Try It Now</h2>



<p class="wp-block-paragraph">Whether you&#8217;re a PowerShell enthusiast, a DevOps engineer, or someone who just wants better desktop control, ClockTray v1.0.0 has something for you. Install it today, explore the CLI, and tell us what you build.</p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/04/23/%f0%9f%95%9b-clocktray-v1-0-0-meet-the-cli-enabled-clock-tool-for-windows/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40251</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/04/clocktray-cli-small.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/clocktray-cli-small.png">
			<media:title type="html">clocktray-cli-small</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/clocktray-cli-small.png?w=512"/>
	</item>
		<item>
		<title>&#128640; Big Update: GPT-Image Models + AI Agent Skills</title>
		<link>https://elbruno.com/2026/04/22/%f0%9f%9a%80-big-update-gpt-image-models-ai-agent-skills/</link>
					<comments>https://elbruno.com/2026/04/22/%f0%9f%9a%80-big-update-gpt-image-models-ai-agent-skills/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Wed, 22 Apr 2026 12:06:09 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[CLI]]></category>
		<category><![CDATA[CLI Tools]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[NuGet]]></category>
		<category><![CDATA[NuGet Packages]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40242</guid>

					<description><![CDATA[Hi! Two weeks ago, I&#160;shipped&#160;t2i&#160;— a terminal-first CLI for text-to-image generation. Today I&#8217;m excited to announce&#160;two major additions&#160;that make&#160;t2i&#160;even more powerful: GPT-Image-1.5 and GPT-Image-2 support — Microsoft&#8217;s DALL-E 3 and next-gen models via Azure OpenAI AI agent skill integration — Teach GitHub Copilot and Claude Code to use t2i automatically TL;DR New models: GPT-Image-1.5 (DALL-E 3) and GPT-Image-2 (next-gen) now [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><a href="https://github.com/elbruno/ElBruno.Text2Image/blob/main/docs/blogs/20260422-gpt-images-and-skills.md#-big-update-gpt-image-models--ai-agent-skills"></a></p>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">Two weeks ago, I&nbsp;<a href="https://file+.vscode-resource.vscode-cdn.net/c%3A/src/ElBruno.Text2Image/docs/blogs/20260420-introducing-t2i-cli.md">shipped&nbsp;<code>t2i</code></a>&nbsp;— a terminal-first CLI for text-to-image generation. Today I&#8217;m excited to announce&nbsp;<strong>two major additions</strong>&nbsp;that make&nbsp;<code>t2i</code>&nbsp;even more powerful:</p>



<ol class="wp-block-list">
<li><strong>GPT-Image-1.5 and GPT-Image-2 support</strong> — Microsoft&#8217;s DALL-E 3 and next-gen models via Azure OpenAI</li>



<li><strong>AI agent skill integration</strong> — Teach GitHub Copilot and Claude Code to use <code>t2i</code> automatically</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">TL;DR</h2>



<ul class="wp-block-list">
<li><strong>New models:</strong> GPT-Image-1.5 (DALL-E 3) and GPT-Image-2 (next-gen) now available via Azure OpenAI</li>



<li><strong>Skill integration:</strong> Run <code>t2i init</code> to teach GitHub Copilot and Claude Code how to generate images autonomously</li>



<li><strong>Updated providers:</strong> Now supporting 5 cloud models (FLUX.2 Pro, FLUX.2 Flex, MAI-Image-2, GPT-Image-1.5, GPT-Image-2)</li>



<li><strong>Version:</strong> Available in <code>t2i</code> v0.16.0+ via <code>dotnet tool update --global ElBruno.Text2Image.Cli</code></li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Part 1: AI Agent Skills — The Biggest Feature</h2>



<p class="wp-block-paragraph">This is the feature I&#8217;m most excited about: <strong>teaching AI agents how to use <code>t2i</code> automatically</strong></p>



<h3 class="wp-block-heading">What Are Skills?</h3>



<p class="wp-block-paragraph">Skills are packages of functionality that AI coding agents can discover and invoke on their own. By installing a skill file, you enable agents like GitHub Copilot and Claude Code to:</p>



<ul class="wp-block-list">
<li><strong>Generate images</strong> directly within your development workflow</li>



<li><strong>Automate batch creation</strong> based on natural language requests</li>



<li><strong>Integrate image generation</strong> into CI/CD pipelines and automation scripts</li>
</ul>



<p class="wp-block-paragraph">Skills work by placing metadata files in well-known directories (<code>.github/skills/</code>&nbsp;for Copilot,&nbsp;<code>.claude/skills/</code>&nbsp;for Claude Code) that agents scan during initialization. Once installed, these agents understand:</p>



<ul class="wp-block-list">
<li>Which <code>t2i</code> commands exist and when to use each one</li>



<li>How to set up secrets safely (env vars first, never commit keys)</li>



<li>The full provider list and which one to default to</li>



<li>Common workflows: first-time setup, single image, batch loops</li>
</ul>



<h3 class="wp-block-heading">How to Set It Up</h3>



<p class="wp-block-paragraph">Install the cli tool locally, setup the tool and from any repository:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i init</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That&#8217;s it. This command writes skill metadata to:</p>



<ul class="wp-block-list">
<li><code>.github/skills/t2i/SKILL.md</code> (for GitHub Copilot)</li>



<li><code>.claude/skills/t2i/SKILL.md</code> (for Claude Code)</li>
</ul>



<p class="wp-block-paragraph">Want only one target?</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i init --target github   # GitHub Copilot only</div><div class="cm-line">t2i init --target claude   # Claude Code only</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The skill files include:</p>



<ul class="wp-block-list">
<li>Tool overview and capabilities</li>



<li>Command syntax with examples</li>



<li>Provider configuration instructions</li>



<li>Best practices and troubleshooting tips</li>
</ul>



<h3 class="wp-block-heading">Real-World Example: GitHub Copilot</h3>



<p class="wp-block-paragraph">After running&nbsp;<code>t2i init --target github</code>, you can interact with Copilot naturally:</p>



<p class="wp-block-paragraph"><strong>You:</strong>&nbsp;&#8220;Generate a futuristic cityscape with neon lights and save it as hero.png&#8221;</p>



<p class="wp-block-paragraph"><strong>Copilot:</strong>&nbsp;<em>Automatically invokes:</em></p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i &quot;futuristic cityscape with neon lights, cyberpunk style, volumetric fog&quot; \</div><div class="cm-line">  --provider foundry-flux2 \</div><div class="cm-line">  --width 1792 \</div><div class="cm-line">  --height 1024 \</div><div class="cm-line">  --output hero.png</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph"><strong>You:</strong>&nbsp;&#8220;Create a series of social media images for our product launch — abstract tech theme&#8221;</p>



<p class="wp-block-paragraph"><strong>Copilot:</strong>&nbsp;<em>Automatically invokes:</em></p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i &quot;abstract tech background with circuit patterns&quot; --output social-1.png</div><div class="cm-line">t2i &quot;geometric tech shapes with gradient colors&quot; --output social-2.png</div><div class="cm-line">t2i &quot;digital network visualization, modern style&quot; --output social-3.png</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">No need to remember the exact syntax or provider flags — Copilot handles it.</p>



<h3 class="wp-block-heading">Real-World Example: Claude Code</h3>



<p class="wp-block-paragraph">After running&nbsp;<code>t2i init --target claude</code>, Claude can drive&nbsp;<code>t2i</code>&nbsp;based on your requests:</p>



<p class="wp-block-paragraph"><strong>You:</strong>&nbsp;&#8220;I need an image of a sunset over mountains for the landing page, wide format&#8221;</p>



<p class="wp-block-paragraph"><strong>Claude:</strong>&nbsp;<em>Automatically invokes:</em></p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i &quot;sunset over mountains, warm golden hour colors, panoramic view&quot; \</div><div class="cm-line">  --provider foundry-flux2 \</div><div class="cm-line">  --width 1792 \</div><div class="cm-line">  --height 1024 \</div><div class="cm-line">  --output landing-hero.png</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph"><strong>You:</strong>&nbsp;&#8220;Generate icon placeholders: home, settings, profile — all square, simple line art&#8221;</p>



<p class="wp-block-paragraph"><strong>Claude:</strong>&nbsp;<em>Automatically invokes multiple commands:</em></p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i &quot;home icon, simple line art, minimalist, 512x512&quot; --out icon-home.png</div><div class="cm-line">t2i &quot;settings icon, simple line art, minimalist, 512x512&quot; --out icon-settings.png</div><div class="cm-line">t2i &quot;profile icon, simple line art, minimalist, 512x512&quot; --out icon-profile.png</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">Why This Matters</h3>



<p class="wp-block-paragraph">Before skills, you had to:</p>



<ol class="wp-block-list">
<li>Remember <code>t2i</code> syntax</li>



<li>Look up provider names</li>



<li>Check configuration flags</li>



<li>Write your own automation scripts</li>
</ol>



<p class="wp-block-paragraph">With skills installed, AI agents become your&nbsp;<strong>image generation assistant</strong>:</p>



<ul class="wp-block-list">
<li>They know the syntax</li>



<li>They pick the right provider</li>



<li>They handle dimensions and output paths</li>



<li>They batch generate when appropriate</li>
</ul>



<p class="wp-block-paragraph">This is especially powerful in CI/CD scenarios. Imagine a GitHub Actions workflow where Copilot autonomously generates marketing assets, social media images, or documentation screenshots based on a simple prompt list.</p>



<p class="wp-block-paragraph"><strong>Configure providers first</strong></p>



<p class="wp-block-paragraph">Before your AI agent can use&nbsp;<code>t2i</code>, ensure at least one provider is configured:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i config   # Interactive setup</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph"><strong>Use environment variables in CI/CD</strong></p>



<p class="wp-block-paragraph">For automated workflows, configure via env vars:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line"># GitHub Actions example</div><div class="cm-line">- name: Generate images</div><div class="cm-line">  env:</div><div class="cm-line">    T2I_FOUNDRY_FLUX2_ENDPOINT: ${{ secrets.FOUNDRY_ENDPOINT }}</div><div class="cm-line">    T2I_FOUNDRY_FLUX2_APIKEY: ${{ secrets.FOUNDRY_APIKEY }}</div><div class="cm-line">  run: |</div><div class="cm-line">    dotnet tool install --global ElBruno.Text2Image.Cli</div><div class="cm-line">    t2i init</div><div class="cm-line">    t2i &quot;hero image for landing page&quot; --provider foundry-flux2 --out assets/hero.png</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph"><strong>Update skills after CLI upgrades</strong></p>



<p class="wp-block-paragraph">When you update&nbsp;<code>t2i</code>, refresh the skill metadata:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet tool update --global ElBruno.Text2Image.Cli</div><div class="cm-line">t2i init   # Regenerates skill files with latest docs</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">More Details</h3>



<p class="wp-block-paragraph">For the complete skill integration guide, including troubleshooting, advanced customization, and platform-specific instructions, see:</p>



<p class="wp-block-paragraph"><strong>→&nbsp;<a href="https://file+.vscode-resource.vscode-cdn.net/c%3A/src/ElBruno.Text2Image/docs/skill-integration.md">docs/skill-integration.md</a></strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f3a8.png" alt="🎨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Part 2: GPT-Image Models — More Choices</h2>



<p class="wp-block-paragraph">Now for the second big update:&nbsp;<strong>GPT-Image-1.5 and GPT-Image-2 support</strong>.</p>



<p class="wp-block-paragraph">These are Microsoft&#8217;s image generation models available via Azure OpenAI Service. Both are based on OpenAI technology but deployed in Azure for enterprise-grade reliability, compliance, and control.</p>



<h3 class="wp-block-heading">GPT-Image-1.5 (DALL-E 3)</h3>



<p class="wp-block-paragraph"><strong>What it is:</strong>&nbsp;Azure OpenAI&#8217;s implementation of OpenAI&#8217;s DALL-E 3 model.</p>



<p class="wp-block-paragraph"><strong>Best for:</strong></p>



<ul class="wp-block-list">
<li><strong>Natural language prompts</strong> — Excellent at understanding complex, conversational descriptions</li>



<li><strong>Photorealistic images</strong> — Great for realistic scenes, portraits, and product photography</li>



<li><strong>Text rendering</strong> — Better at including readable text in images (though still not perfect)</li>



<li><strong>Enterprise compliance</strong> — Deployed in your Azure region with full data residency</li>
</ul>



<p class="wp-block-paragraph"><strong>Supported sizes:</strong></p>



<ul class="wp-block-list">
<li>1024×1024 (square)</li>



<li>1792×1024 (landscape)</li>



<li>1024×1792 (portrait)</li>
</ul>



<p class="wp-block-paragraph"><strong>Example use cases:</strong></p>



<ul class="wp-block-list">
<li>Marketing visuals with text overlays</li>



<li>Product mockups and packaging designs</li>



<li>Editorial illustrations for blog posts</li>



<li>Social media graphics</li>
</ul>



<h3 class="wp-block-heading">GPT-Image-2 (Next-Gen)</h3>



<p class="wp-block-paragraph"><strong>What it is:</strong>&nbsp;Microsoft&#8217;s next-generation image model — more advanced than DALL-E 3.</p>



<p class="wp-block-paragraph"><strong>Best for:</strong></p>



<ul class="wp-block-list">
<li><strong>High-quality artistic images</strong> — Improved coherence and style consistency</li>



<li><strong>Complex compositions</strong> — Better at multi-object scenes with detailed relationships</li>



<li><strong>Stylized rendering</strong> — Excels at specific art styles (watercolor, oil painting, digital art)</li>



<li><strong>Prompt adherence</strong> — Follows instructions more accurately, especially for abstract concepts</li>
</ul>



<p class="wp-block-paragraph"><strong>Supported sizes:</strong></p>



<ul class="wp-block-list">
<li>1024×1024 (square)</li>



<li>1792×1024 (landscape)</li>



<li>1024×1792 (portrait)</li>
</ul>



<p class="wp-block-paragraph"><strong>Example use cases:</strong></p>



<ul class="wp-block-list">
<li>Concept art and game design</li>



<li>Artistic book cover designs</li>



<li>Abstract and stylized illustrations</li>



<li>Character design and visual development</li>
</ul>



<h3 class="wp-block-heading">How to Use Them</h3>



<p class="wp-block-paragraph">Both models use the&nbsp;<strong>Azure OpenAI Service</strong>, so you need:</p>



<ol class="wp-block-list">
<li>An Azure subscription</li>



<li>An Azure OpenAI resource</li>



<li>A deployment of <code>gpt-image-1.5</code> or <code>gpt-image-2</code></li>



<li>Your endpoint URL and API key</li>
</ol>



<h4 class="wp-block-heading">Quick Setup (GPT-Image-1.5)</h4>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line"># Interactive setup wizard</div><div class="cm-line">t2i config set foundry-gpt-image-1p5</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The wizard prompts for:</p>



<ul class="wp-block-list">
<li><strong>Endpoint URL</strong> (e.g., <code>https://my-resource.openai.azure.com/</code>)</li>



<li><strong>API Key</strong> (from Azure Portal)</li>



<li><strong>Deployment name</strong> (e.g., <code>gpt-image-15</code>)</li>
</ul>



<p class="wp-block-paragraph">Then generate:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i &quot;an impressionist painting of a garden in spring&quot; \</div><div class="cm-line">  --provider foundry-gpt-image-1p5</div></code></pre>
		</div>
	</div>
</div>


<h4 class="wp-block-heading">Quick Setup (GPT-Image-2)</h4>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line"># Interactive setup wizard</div><div class="cm-line">t2i config set foundry-gpt-image-2</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then generate:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i &quot;a sci-fi space station orbiting a ringed planet, digital art&quot; \</div><div class="cm-line">  --provider foundry-gpt-image-2</div></code></pre>
		</div>
	</div>
</div>


<h4 class="wp-block-heading">PowerShell Examples</h4>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line"># GPT-Image-1.5: Photorealistic product shot</div><div class="cm-line">t2i &quot;professional product photo of a smartwatch on marble, studio lighting&quot; `</div><div class="cm-line">  --provider foundry-gpt-image-1p5 `</div><div class="cm-line">  --width 1792 `</div><div class="cm-line">  --height 1024 `</div><div class="cm-line">  --output product-hero.png</div><div class="cm-line"></div><div class="cm-line"># GPT-Image-2: Abstract art for website header</div><div class="cm-line">t2i &quot;abstract geometric patterns with vibrant gradients, modern tech aesthetic&quot; `</div><div class="cm-line">  --provider foundry-gpt-image-2 `</div><div class="cm-line">  --output header-bg.png</div></code></pre>
		</div>
	</div>
</div>


<h4 class="wp-block-heading">Bash Examples</h4>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line"># GPT-Image-1.5: Editorial illustration</div><div class="cm-line">t2i &quot;a serene lake at sunrise with mountains in the distance, photorealistic&quot; \</div><div class="cm-line">  --provider foundry-gpt-image-1p5 \</div><div class="cm-line">  --width 1792 \</div><div class="cm-line">  --height 1024 \</div><div class="cm-line">  --output editorial.png</div><div class="cm-line"></div><div class="cm-line"># GPT-Image-2: Character concept art</div><div class="cm-line">t2i &quot;character concept art of a futuristic knight, armor with neon accents, digital painting&quot; \</div><div class="cm-line">  --provider foundry-gpt-image-2 \</div><div class="cm-line">  --width 1024 \</div><div class="cm-line">  --height 1792 \</div><div class="cm-line">  --output character.png</div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">Switching Models</h3>



<p class="wp-block-paragraph">You can set a default provider in your config:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line"># Use GPT-Image-2 as default</div><div class="cm-line">t2i config set default-provider foundry-gpt-image-2</div><div class="cm-line"></div><div class="cm-line"># Now this uses GPT-Image-2</div><div class="cm-line">t2i &quot;your prompt here&quot;</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Or specify per-command:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line"># Compare outputs from different models</div><div class="cm-line">t2i &quot;a cyberpunk cityscape&quot; --provider foundry-flux2 --output flux-city.png</div><div class="cm-line">t2i &quot;a cyberpunk cityscape&quot; --provider foundry-gpt-image-2 --output gpt-city.png</div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">Complete Provider List</h3>



<p class="wp-block-paragraph">Here&#8217;s the full lineup after this update:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Provider ID</th><th class="has-text-align-left" data-align="left">Model</th><th class="has-text-align-left" data-align="left">Provider</th><th class="has-text-align-left" data-align="left">Best For</th></tr></thead><tbody><tr><td class="has-text-align-left" data-align="left"><code>foundry-flux2</code></td><td class="has-text-align-left" data-align="left">FLUX.2 Pro</td><td class="has-text-align-left" data-align="left">Microsoft Foundry</td><td class="has-text-align-left" data-align="left">Photorealistic images, fine control</td></tr><tr><td class="has-text-align-left" data-align="left"><code>foundry-flux2</code>&nbsp;(Flex)</td><td class="has-text-align-left" data-align="left">FLUX.2 Flex</td><td class="has-text-align-left" data-align="left">Microsoft Foundry</td><td class="has-text-align-left" data-align="left">Text-heavy designs, logos</td></tr><tr><td class="has-text-align-left" data-align="left"><code>foundry-mai2</code></td><td class="has-text-align-left" data-align="left">MAI-Image-2</td><td class="has-text-align-left" data-align="left">Microsoft Foundry</td><td class="has-text-align-left" data-align="left">Fast iteration, rich prompts</td></tr><tr><td class="has-text-align-left" data-align="left"><code>foundry-gpt-image-1p5</code></td><td class="has-text-align-left" data-align="left">GPT-Image-1.5</td><td class="has-text-align-left" data-align="left">Azure OpenAI</td><td class="has-text-align-left" data-align="left">Natural language, photorealism</td></tr><tr><td class="has-text-align-left" data-align="left"><code>foundry-gpt-image-2</code></td><td class="has-text-align-left" data-align="left">GPT-Image-2</td><td class="has-text-align-left" data-align="left">Azure OpenAI</td><td class="has-text-align-left" data-align="left">Next-gen quality, style consistency</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>How to choose:</strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4ca.png" alt="📊" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Model Comparison</h2>



<p class="wp-block-paragraph">Here&#8217;s a quick comparison to help you choose:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Feature</th><th class="has-text-align-left" data-align="left">FLUX.2 Pro</th><th class="has-text-align-left" data-align="left">MAI-Image-2</th><th class="has-text-align-left" data-align="left">GPT-Image-1.5</th><th class="has-text-align-left" data-align="left">GPT-Image-2</th></tr></thead><tbody><tr><td class="has-text-align-left" data-align="left"><strong>Quality</strong></td><td class="has-text-align-left" data-align="left"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td class="has-text-align-left" data-align="left"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td class="has-text-align-left" data-align="left"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td class="has-text-align-left" data-align="left"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td></tr><tr><td class="has-text-align-left" data-align="left"><strong>Speed</strong></td><td class="has-text-align-left" data-align="left">~30-40s</td><td class="has-text-align-left" data-align="left">~15-20s</td><td class="has-text-align-left" data-align="left">~8-12s</td><td class="has-text-align-left" data-align="left">~8-12s</td></tr><tr><td class="has-text-align-left" data-align="left"><strong>Photorealism</strong></td><td class="has-text-align-left" data-align="left">Excellent</td><td class="has-text-align-left" data-align="left">Good</td><td class="has-text-align-left" data-align="left">Excellent</td><td class="has-text-align-left" data-align="left">Excellent</td></tr><tr><td class="has-text-align-left" data-align="left"><strong>Artistic Styles</strong></td><td class="has-text-align-left" data-align="left">Good</td><td class="has-text-align-left" data-align="left">Excellent</td><td class="has-text-align-left" data-align="left">Good</td><td class="has-text-align-left" data-align="left">Excellent</td></tr><tr><td class="has-text-align-left" data-align="left"><strong>Text Rendering</strong></td><td class="has-text-align-left" data-align="left">Poor</td><td class="has-text-align-left" data-align="left">Fair</td><td class="has-text-align-left" data-align="left">Good</td><td class="has-text-align-left" data-align="left">Good</td></tr><tr><td class="has-text-align-left" data-align="left"><strong>Prompt Adherence</strong></td><td class="has-text-align-left" data-align="left">Excellent</td><td class="has-text-align-left" data-align="left">Very Good</td><td class="has-text-align-left" data-align="left">Very Good</td><td class="has-text-align-left" data-align="left">Excellent</td></tr><tr><td class="has-text-align-left" data-align="left"><strong>Custom Sizes</strong></td><td class="has-text-align-left" data-align="left"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Any size</td><td class="has-text-align-left" data-align="left"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Any size</td><td class="has-text-align-left" data-align="left"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Fixed sizes</td><td class="has-text-align-left" data-align="left"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Fixed sizes</td></tr><tr><td class="has-text-align-left" data-align="left"><strong>API Type</strong></td><td class="has-text-align-left" data-align="left">Async (polling)</td><td class="has-text-align-left" data-align="left">Async (polling)</td><td class="has-text-align-left" data-align="left">Sync</td><td class="has-text-align-left" data-align="left">Sync</td></tr><tr><td class="has-text-align-left" data-align="left"><strong>Provider</strong></td><td class="has-text-align-left" data-align="left">Microsoft Foundry</td><td class="has-text-align-left" data-align="left">Microsoft Foundry</td><td class="has-text-align-left" data-align="left">Azure OpenAI</td><td class="has-text-align-left" data-align="left">Azure OpenAI</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>Performance notes:</strong></p>



<ul class="wp-block-list">
<li>FLUX.2 and MAI models use asynchronous polling (submit → poll → retrieve)</li>



<li>GPT-Image models use synchronous API (submit → wait → receive)</li>



<li>First request may be slower due to model warm-up</li>



<li>Batch jobs benefit from parallel requests</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f504.png" alt="🔄" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Migration Guide</h2>



<p class="wp-block-paragraph">If you&#8217;re already using&nbsp;<code>t2i</code>&nbsp;with FLUX.2 or MAI-Image-2, upgrading is straightforward.</p>



<h3 class="wp-block-heading">Step 1: Update the CLI</h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet tool update --global ElBruno.Text2Image.Cli</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Verify:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i --version</div><div class="cm-line"># Should show v0.16.0 or later</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">Step 2: List New Providers</h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i providers</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">You should see:</p>



<ul class="wp-block-list">
<li><code>foundry-gpt-image-1p5</code> — GPT-Image-1.5 / DALL-E 3</li>



<li><code>foundry-gpt-image-2</code> — GPT-Image-2 (Next-Gen)</li>
</ul>



<h3 class="wp-block-heading">Step 3: Configure GPT Models (Optional)</h3>



<p class="wp-block-paragraph">If you want to use GPT-Image models:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i config set foundry-gpt-image-1p5   # Configure GPT-Image-1.5</div><div class="cm-line">t2i config set foundry-gpt-image-2     # Configure GPT-Image-2</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">Step 4: Update Skill Files</h3>



<p class="wp-block-paragraph">If you previously ran&nbsp;<code>t2i init</code>, refresh your skill metadata:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i init --force   # Overwrites existing skill files</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">This ensures GitHub Copilot and Claude Code know about the new models.</p>



<h3 class="wp-block-heading">Step 5: Test</h3>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i doctor</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Check that all providers show as &#8220;configured&#8221; and &#8220;healthy.&#8221;</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> What&#8217;s Next</h2>



<p class="wp-block-paragraph">This release adds major capabilities, but there&#8217;s more coming:</p>



<p class="wp-block-paragraph"><strong>v0.17.0+ (Q2 2026):</strong></p>



<ul class="wp-block-list">
<li><strong>Local inference edition</strong> — CPU, CUDA, DirectML providers (no cloud required)</li>



<li><strong>Model marketplace</strong> — Download and manage local ONNX models</li>



<li><strong>Batch API</strong> — Submit multiple prompts in one call</li>



<li><strong>Image-to-image</strong> — Use existing images as input for variations</li>
</ul>



<p class="wp-block-paragraph"><strong>Community-requested features:</strong></p>



<ul class="wp-block-list">
<li><strong>Negative prompts</strong> — Specify what <em>not</em> to include</li>



<li><strong>Style presets</strong> — Quick templates (e.g., <code>--style cinematic</code>)</li>



<li><strong>Config profiles</strong> — Switch between dev/prod configurations</li>



<li><strong>Web UI</strong> — Optional browser-based interface</li>
</ul>



<p class="wp-block-paragraph">Want to influence the roadmap? File a feature request:</p>



<p class="wp-block-paragraph"><strong>→&nbsp;<a href="https://github.com/elbruno/ElBruno.Text2Image/issues">github.com/elbruno/ElBruno.Text2Image/issues</a></strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Links &amp; Resources</h2>



<p class="wp-block-paragraph"><strong>Documentation:</strong></p>



<ul class="wp-block-list">
<li><a href="https://file+.vscode-resource.vscode-cdn.net/c%3A/src/ElBruno.Text2Image/docs/cli-tool.md">CLI Tool Reference</a></li>



<li><a href="https://file+.vscode-resource.vscode-cdn.net/c%3A/src/ElBruno.Text2Image/docs/skill-integration.md">Skill Integration Guide</a></li>



<li><a href="https://file+.vscode-resource.vscode-cdn.net/c%3A/src/ElBruno.Text2Image/docs/gpt-image-1p5-setup-guide.md">GPT-Image-1.5 Setup Guide</a></li>
</ul>



<p class="wp-block-paragraph"><strong>Installation:</strong></p>



<ul class="wp-block-list">
<li><a href="https://www.nuget.org/packages/ElBruno.Text2Image.Cli">NuGet Package</a></li>



<li><a href="https://github.com/elbruno/ElBruno.Text2Image/releases">GitHub Releases</a></li>
</ul>



<p class="wp-block-paragraph"><strong>Community:</strong></p>



<ul class="wp-block-list">
<li><a href="https://github.com/elbruno/ElBruno.Text2Image">GitHub Repository</a></li>



<li><a href="https://github.com/elbruno/ElBruno.Text2Image/issues">Report Issues</a></li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<p class="wp-block-paragraph"><strong>Model:</strong>&nbsp;GPT-Image-2<br /><strong>Size:</strong>&nbsp;1792×1024 (landscape)<br /><strong>Generation time:</strong>&nbsp;~9.2 seconds<br /><strong>Cost:</strong>&nbsp;~$0.15</p>



<p class="wp-block-paragraph">This demonstrates GPT-Image-2&#8217;s ability to understand abstract concepts (a terminal &#8220;emitting&#8221; images) and render them with artistic style.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<p class="wp-block-paragraph">Questions? Feature requests? Found a bug?</p>



<p class="wp-block-paragraph"><strong>→&nbsp;<a href="https://github.com/elbruno/ElBruno.Text2Image/issues">github.com/elbruno/ElBruno.Text2Image/issues</a></strong></p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/04/22/%f0%9f%9a%80-big-update-gpt-image-models-ai-agent-skills/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40242</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/02/chatgpt-image-feb-26-2026-01_13_39-pm.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/02/chatgpt-image-feb-26-2026-01_13_39-pm.png">
			<media:title type="html">ChatGPT Image Feb 26, 2026, 01_13_39 PM</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>
	</item>
		<item>
		<title>&#128640; Meet t2i — The ElBruno.Text2Image CLI</title>
		<link>https://elbruno.com/2026/04/20/%f0%9f%9a%80-meet-t2i-the-elbruno-text2image-cli/</link>
					<comments>https://elbruno.com/2026/04/20/%f0%9f%9a%80-meet-t2i-the-elbruno-text2image-cli/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 15:40:04 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[CLI]]></category>
		<category><![CDATA[CLI Tools]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[NuGet]]></category>
		<category><![CDATA[Skills]]></category>
		<category><![CDATA[Text-To-Image]]></category>
		<category><![CDATA[Tools]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40237</guid>

					<description><![CDATA[Hi! I just shipped t2i, a terminal-first CLI tool for ElBruno.Text2Image. Generate images from your shell in two commands — no UI, no browser, just a simple cli interface to image generation from the cloud. This is the&#160;Lite edition&#160;(cloud-only, ~2.4 MB on NuGet) — perfect for CI/CD pipelines, deployment scripts, batch jobs, and developers who live [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" width="640" height="384" src="https://elbruno.com/wp-content/uploads/2026/04/t2i-50.png?w=640" alt="" class="wp-image-40239" srcset="https://elbruno.com/wp-content/uploads/2026/04/t2i-50.png 640w, https://elbruno.com/wp-content/uploads/2026/04/t2i-50.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/04/t2i-50.png?w=300 300w" sizes="auto, (max-width: 640px) 100vw, 640px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">I just shipped <strong>t2i</strong>, a terminal-first CLI tool for ElBruno.Text2Image. Generate images from your shell in two commands — no UI, no browser, just a simple cli interface to image generation from the cloud.</p>



<p class="wp-block-paragraph">This is the&nbsp;<strong>Lite edition</strong>&nbsp;(cloud-only, ~2.4 MB on NuGet) — perfect for CI/CD pipelines, deployment scripts, batch jobs, and developers who live in the terminal.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f6e0.png" alt="🛠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Install</h2>



<h3 class="wp-block-heading">Option 1: .NET Tool (recommended)</h3>



<p class="wp-block-paragraph">If you have .NET 8+ installed:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet tool install --global ElBruno.Text2Image.Cli</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then use&nbsp;<code>t2i</code>&nbsp;from anywhere on your machine.</p>



<h3 class="wp-block-heading">Option 2: Self-Contained Binaries</h3>



<p class="wp-block-paragraph">No .NET? Download pre-built binaries from the&nbsp;<a href="https://github.com/elbruno/ElBruno.Text2Image/releases/tag/cli-v0.10.0">release page</a>. Currently available for:</p>



<ul class="wp-block-list">
<li><strong>Windows x64</strong></li>



<li><strong>Linux x64</strong></li>



<li><strong>macOS arm64</strong> (Intel coming back soon)</li>
</ul>



<p class="wp-block-paragraph">Just extract and run. One file, no dependencies.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> First Image in 30 Seconds</h2>



<p class="wp-block-paragraph">Ready to generate? Two commands:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i config</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">This launches an interactive setup wizard (powered by&nbsp;<a href="https://spectreconsole.net/">Spectre.Console</a>, so it&#8217;s pretty). Pick your provider, enter your API key, and you&#8217;re done. The CLI stores everything securely.</p>



<p class="wp-block-paragraph">Then generate:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i &quot;a robot painting a landscape&quot;</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That&#8217;s it. The image appears in your current directory.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f510.png" alt="🔐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Where Do My Secrets Live?</h2>



<p class="wp-block-paragraph">Short version:&nbsp;<code>t2i config</code>&nbsp;stores credentials securely on your machine — DPAPI-encrypted on Windows,&nbsp;<code>0600</code>-permissioned file on macOS/Linux. Env vars are for CI only. CLI&nbsp;<code>--api-key</code>&nbsp;is for one-off tests.</p>



<p class="wp-block-paragraph">For the full resolution chain, CI/CD guidance, and the do/don&#8217;t table, see&nbsp;<strong><a href="https://file+.vscode-resource.vscode-cdn.net/c%3A/src/ElBruno.Text2Image/docs/cli-secrets.md">docs/cli-secrets.md</a></strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2601.png" alt="☁" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Providers in Lite</h2>



<p class="wp-block-paragraph">This release ships with two cloud providers on Azure AI Foundry:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Provider</th><th class="has-text-align-left" data-align="left">Model</th><th class="has-text-align-left" data-align="left">Best For</th></tr></thead><tbody><tr><td class="has-text-align-left" data-align="left"><code>foundry-flux2</code></td><td class="has-text-align-left" data-align="left">FLUX.2 Pro</td><td class="has-text-align-left" data-align="left">High-quality, fine control, batch jobs</td></tr><tr><td class="has-text-align-left" data-align="left"><code>foundry-mai2</code></td><td class="has-text-align-left" data-align="left">MAI-Image-2</td><td class="has-text-align-left" data-align="left">Synchronous API, rich prompts, fast iteration</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">More providers (Anthropic, OpenAI) coming in future releases.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4cb.png" alt="📋" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Useful Commands</h2>



<p class="wp-block-paragraph">Here&#8217;s the CLI cheat sheet:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line"># List available providers</div><div class="cm-line">t2i providers</div><div class="cm-line"></div><div class="cm-line"># Run health checks (config + API connectivity)</div><div class="cm-line">t2i doctor</div><div class="cm-line"></div><div class="cm-line"># Show stored secrets (redacted)</div><div class="cm-line">t2i secrets list</div><div class="cm-line"></div><div class="cm-line"># Display version + commit SHA</div><div class="cm-line">t2i version</div><div class="cm-line"></div><div class="cm-line"># Interactive config setup</div><div class="cm-line">t2i config</div><div class="cm-line"></div><div class="cm-line"># Generate with default provider</div><div class="cm-line">t2i &quot;a cyberpunk cityscape at night&quot;</div><div class="cm-line"></div><div class="cm-line"># Generate with specific provider, dimensions, and output file</div><div class="cm-line">t2i &quot;my prompt&quot; \</div><div class="cm-line">  --provider foundry-mai2 \</div><div class="cm-line">  --width 1024 \</div><div class="cm-line">  --height 1024 \</div><div class="cm-line">  --output ./my-image.png</div><div class="cm-line"></div><div class="cm-line"># Show help</div><div class="cm-line">t2i --help</div><div class="cm-line">t2i generate --help</div><div class="cm-line"></div><div class="cm-line"># Teach your AI agent</div><div class="cm-line">t2i init</div></code></pre>
		</div>
	</div>
</div>


<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f504.png" alt="🔄" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Switching Models (v0.10.0+)</h2>



<p class="wp-block-paragraph">Both providers support multiple model variants. By default,&nbsp;<code>foundry-mai2</code>&nbsp;uses&nbsp;<code>MAI-Image-2</code>&nbsp;and&nbsp;<code>foundry-flux2</code>&nbsp;uses&nbsp;<code>FLUX.2-pro</code>. To switch models:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line"># Use MAI-Image-2e</div><div class="cm-line">t2i config set foundry-mai2.model MAI-Image-2e</div><div class="cm-line"></div><div class="cm-line"># Use FLUX.2 Flex for text-heavy design and logos</div><div class="cm-line">t2i config set foundry-flux2.model FLUX.2-flex</div><div class="cm-line"></div><div class="cm-line"># View your configuration</div><div class="cm-line">t2i config show</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The&nbsp;<code>config show</code>&nbsp;command displays your endpoint and model in plain text, with only the API key masked. This makes it easy to verify your setup without revealing sensitive credentials.</p>



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Teach Your AI Agent —&nbsp;<code>t2i init</code></h2>



<p class="wp-block-paragraph">Inspired by&nbsp;<a href="https://aspire.dev/get-started/ai-coding-agents/">Aspire&#8217;s agent init pattern</a>, t2i now ships with a skill file your AI coding agent can read. Run this in any repo:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i init</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That writes a&nbsp;<code>SKILL.md</code>&nbsp;to both&nbsp;<code>.github/skills/t2i/</code>&nbsp;and&nbsp;<code>.claude/skills/t2i/</code>. From that point on, GitHub Copilot, Claude Code, and any MCP-aware agent know:</p>



<ul class="wp-block-list">
<li>Which <code>t2i</code> commands exist and when to use each one</li>



<li>How to set up secrets safely (env vars first, never commit keys)</li>



<li>The full provider list and which one to default to</li>



<li>Common workflows: first-time setup, single image, batch loops</li>
</ul>



<p class="wp-block-paragraph">Want only one target?</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i init --target github   # only .github/skills/t2i/</div><div class="cm-line">t2i init --target claude   # only .claude/skills/t2i/</div><div class="cm-line">t2i init --force           # overwrite existing skill files</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The canonical version of this skill also lives in this repo at&nbsp;<code>.github/skills/t2i/SKILL.md</code>&nbsp;— that means if you open the ElBruno.Text2Image source itself in Copilot or Claude Code, your agent already knows how to drive the CLI.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4e6.png" alt="📦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Coming Soon — Other Platforms</h2>



<h3 class="wp-block-heading">winget</h3>



<p class="wp-block-paragraph">The manifest stub is already in the repo (<code>winget/manifests/E/ElBruno/Text2Image/0.10.0/</code>). First submission to&nbsp;<a href="https://github.com/microsoft/winget-pkgs">microsoft/winget-pkgs</a>&nbsp;is queued. Automation will come in v0.2.0.</p>



<h3 class="wp-block-heading">Homebrew</h3>



<p class="wp-block-paragraph">Tap&nbsp;<code>elbruno/elbruno</code>&nbsp;is planned for v0.2.0.</p>



<h3 class="wp-block-heading">macOS Intel (osx-x64)</h3>



<p class="wp-block-paragraph">GitHub&#8217;s macOS-13 runner queue has been a bit slow lately. Intel Mac users should use&nbsp;<code>dotnet tool install</code>&nbsp;for now. The self-contained binary will return once the runners stabilize.</p>



<h3 class="wp-block-heading">Full Edition (Local GPU)</h3>



<p class="wp-block-paragraph">The&nbsp;<strong>Cli.Full</strong>&nbsp;edition — supporting local inference with ONNX Runtime (CPU, CUDA, DirectML, NPU) — is coming in a future release. ~200 MB, separate package.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f3a8.png" alt="🎨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Sample Usages</h2>



<p class="wp-block-paragraph">Here are some fun one-liners to try:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">t2i &quot;a cyberpunk taco truck at sunset, cinematic lighting, volumetric fog&quot;</div><div class="cm-line"></div><div class="cm-line">t2i &quot;a low-poly 3D render of a friendly robot waving&quot; --provider foundry-mai2</div><div class="cm-line"></div><div class="cm-line">t2i &quot;minimalist line art of a cat reading a book&quot; --width 1024 --height 1024 --output cat.png</div><div class="cm-line"></div><div class="cm-line">t2i &quot;watercolor painting of the Madrid skyline at dusk, golden hour&quot; --output madrid.png</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph"><strong>Tip:</strong>&nbsp;The better your prompt, the better the image. Be specific about style (cinematic, watercolor, line art), mood (peaceful, energetic), and composition (wide-angle, close-up). Models reward detail.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f91d.png" alt="🤝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Let&#8217;s Go</h2>



<p class="wp-block-paragraph"><a href="https://www.nuget.org/packages/ElBruno.Text2Image.Cli/0.1.0">Download it now</a>, run&nbsp;<code>t2i config</code>, and start generating. Found a bug?&nbsp;<a href="https://github.com/elbruno/ElBruno.Text2Image/issues">File an issue</a>.</p>



<p class="wp-block-paragraph"><strong>Links:</strong></p>



<ul class="wp-block-list">
<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4e6.png" alt="📦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>NuGet:</strong> <a href="https://www.nuget.org/packages/ElBruno.Text2Image.Cli/0.10.0">ElBruno.Text2Image.Cli/0.10.0</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4c2.png" alt="📂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Release:</strong> <a href="https://github.com/elbruno/ElBruno.Text2Image/releases/tag/cli-v0.10.0">github.com/elbruno/ElBruno.Text2Image/releases/tag/cli-v0.10.0</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4d6.png" alt="📖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Full Docs:</strong> <a href="https://github.com/elbruno/ElBruno.Text2Image/blob/main/docs/cli-tool.md">docs/cli-tool.md</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f5bc.png" alt="🖼" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Hero image prompt:</strong> <a href="https://github.com/elbruno/ElBruno.Text2Image/blob/main/docs/blogs/20260420-introducing-t2i-cli-image-prompt.md">docs/blogs/20260420-introducing-t2i-cli-image-prompt.md</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f41b.png" alt="🐛" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Issues:</strong> <a href="https://github.com/elbruno/ElBruno.Text2Image/issues">github.com/elbruno/ElBruno.Text2Image/issues</a></li>
</ul>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/04/20/%f0%9f%9a%80-meet-t2i-the-elbruno-text2image-cli/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40237</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/04/t2i-50.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/t2i-50.png">
			<media:title type="html">t2i-50</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/t2i-50.png?w=640"/>
	</item>
		<item>
		<title>&#128506;️ AOT-Friendly DTO Mapping in .NET</title>
		<link>https://elbruno.com/2026/04/16/%f0%9f%97%ba%ef%b8%8f-aot-friendly-dto-mapping-in-net/</link>
					<comments>https://elbruno.com/2026/04/16/%f0%9f%97%ba%ef%b8%8f-aot-friendly-dto-mapping-in-net/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Thu, 16 Apr 2026 22:13:46 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[.Net]]></category>
		<category><![CDATA[AOT]]></category>
		<category><![CDATA[AOT Mapper]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[Data Mapping]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[NuGet]]></category>
		<category><![CDATA[NuGet Packages]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40228</guid>

					<description><![CDATA[Hi! So, I&#8217;ve been away from any scenario that involves and needs data mapping, until I get to OpenClawNet. So I got to the point that I need to map my entities to DTOs? I read this a lot &#62;&#62; runtime reflection doesn&#8217;t play nice with AOT and trimming. 😅 So I SQUAD myselft and built: 👉 ElBruno.AotMapper [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="540" src="https://elbruno.com/wp-content/uploads/2026/04/gemini_generated_image_7a9gt7a9gt7a9gt7.png?w=1024" alt="" class="wp-image-40230" srcset="https://elbruno.com/wp-content/uploads/2026/04/gemini_generated_image_7a9gt7a9gt7a9gt7.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/04/gemini_generated_image_7a9gt7a9gt7a9gt7.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/04/gemini_generated_image_7a9gt7a9gt7a9gt7.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/04/gemini_generated_image_7a9gt7a9gt7a9gt7.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/04/gemini_generated_image_7a9gt7a9gt7a9gt7.png 1424w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">So, I&#8217;ve been away from any scenario that involves and needs data mapping, until I get to <a href="https://www.linkedin.com/feed/update/urn:li:activity:7449425001541386241/">OpenClawNet</a>. So I got to the point that I need to map my entities to DTOs?</p>



<p class="wp-block-paragraph">I read this a lot &gt;&gt; <strong>runtime reflection doesn&#8217;t play nice with AOT and trimming.</strong> <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">So I SQUAD myselft and built:</p>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong><a href="https://www.nuget.org/packages/ElBruno.AotMapper" target="_blank" rel="noreferrer noopener">ElBruno.AotMapper</a></strong></p>



<p class="wp-block-paragraph">A Roslyn source-generator library that creates all your mapping code&nbsp;<strong>at compile time</strong>. No reflection. No dynamic IL. Just clean, generated C# that works everywhere — including NativeAOT and trimmed apps.</p>



<p class="wp-block-paragraph"><em><strong>Note: </strong>this is a WIP, so I expect to learn more during my usage. If you find any issue or have any ideas, please drop an issue on the repo, or even better &gt; a PR with some cool fixes / updates!</em></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<p class="wp-block-paragraph">Let me show you how it works. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2b07.png" alt="⬇" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4e6.png" alt="📦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Install</h2>



<p class="wp-block-paragraph">Two packages — the core attributes and the generator:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet add package ElBruno.AotMapper</div><div class="cm-line">dotnet add package ElBruno.AotMapper.Generator</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">That&#8217;s it. The generator runs during build and creates extension methods for you automatically.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> The Simplest Mapping — One Attribute, Done</h2>



<p class="wp-block-paragraph">Let&#8217;s start with the basics. You have a&nbsp;<code>Product</code>&nbsp;entity and you need a DTO:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">AotMapper</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-variableName tok-definition">Product</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">public</span> <span class="tok-typeName">int</span> <span class="tok-variableName">Id</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">set</span>; }</div><div class="cm-line">    <span class="tok-keyword">public</span> <span class="tok-typeName">string</span> <span class="tok-variableName">Name</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">set</span>; } <span class="tok-operator">=</span> <span class="tok-typeName">string</span>.<span class="tok-variableName">Empty</span>;</div><div class="cm-line">    <span class="tok-keyword">public</span> <span class="tok-typeName">decimal</span> <span class="tok-variableName">Price</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">set</span>; }</div><div class="cm-line">    <span class="tok-keyword">public</span> <span class="tok-variableName">ProductCategory</span> <span class="tok-variableName">Category</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">set</span>; }</div><div class="cm-line">    <span class="tok-keyword">public</span> <span class="tok-variableName">List</span><span class="tok-operator">&lt;</span><span class="tok-typeName">string</span><span class="tok-operator">&gt;</span> <span class="tok-variableName">Tags</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">set</span>; } <span class="tok-operator">=</span> <span class="tok-keyword">new</span>();</div><div class="cm-line">}</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">public</span> <span class="tok-keyword">enum</span> <span class="tok-variableName">ProductCategory</span> { <span class="tok-variableName">Library</span>, <span class="tok-variableName">Tool</span>, <span class="tok-variableName">Framework</span> }</div><div class="cm-line"></div><div class="cm-line">[<span class="tok-variableName">MapFrom</span>(<span class="tok-keyword">typeof</span>(<span class="tok-variableName">Product</span>))]</div><div class="cm-line"><span class="tok-keyword">public</span> <span class="tok-keyword">partial</span> <span class="tok-keyword">record</span> <span class="tok-variableName tok-definition">ProductDto</span>(<span class="tok-typeName">int</span> <span class="tok-variableName">Id</span>, <span class="tok-typeName">string</span> <span class="tok-variableName">Name</span>, <span class="tok-typeName">decimal</span> <span class="tok-variableName">Price</span>, <span class="tok-typeName">string</span> <span class="tok-variableName">Category</span>, <span class="tok-variableName">List</span><span class="tok-operator">&lt;</span><span class="tok-typeName">string</span><span class="tok-operator">&gt;</span> <span class="tok-variableName">Tags</span>);</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">See that&nbsp;<code>[MapFrom]</code>&nbsp;attribute? That&#8217;s the magic. The generator sees it, matches properties by name, and creates a&nbsp;<code>.ToProductDto()</code>&nbsp;extension method for you.</p>



<p class="wp-block-paragraph">Now you use it like this:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">product</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">Product</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-variableName">Id</span> <span class="tok-operator">=</span> <span class="tok-number">42</span>,</div><div class="cm-line">    <span class="tok-variableName">Name</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;ElBruno.AotMapper&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">Price</span> <span class="tok-operator">=</span> <span class="tok-number">0.0</span><span class="tok-variableName">m</span>,</div><div class="cm-line">    <span class="tok-variableName">Category</span> <span class="tok-operator">=</span> <span class="tok-variableName">ProductCategory</span>.<span class="tok-variableName">Library</span>,</div><div class="cm-line">    <span class="tok-variableName">Tags</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">List</span><span class="tok-operator">&lt;</span><span class="tok-typeName">string</span><span class="tok-operator">&gt;</span> { <span class="tok-string">&quot;aot&quot;</span>, <span class="tok-string">&quot;mapper&quot;</span>, <span class="tok-string">&quot;dotnet&quot;</span> }</div><div class="cm-line">};</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">dto</span> <span class="tok-operator">=</span> <span class="tok-variableName">product</span>.<span class="tok-variableName">ToProductDto</span>();</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Product: {dto.Name} (#{dto.Id})&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;  Category: {dto.Category}&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;  Tags: {string.Join(&quot;</span>, <span class="tok-string">&quot;, dto.Tags)}&quot;</span>);</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">I forgot how fun this was &gt; n<strong>o reflection, no runtime overhead,  AOT safe (I think, still checking stuff here) <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f500.png" alt="🔀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Remapping Properties with&nbsp;<code>[MapProperty]</code></h2>



<p class="wp-block-paragraph">What if your source and destination properties don&#8217;t have the same name? Easy — use&nbsp;<code>[MapProperty]</code>:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">AotMapper</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-variableName tok-definition">Customer</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">public</span> <span class="tok-typeName">int</span> <span class="tok-variableName">Id</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">set</span>; }</div><div class="cm-line">    <span class="tok-keyword">public</span> <span class="tok-typeName">string</span> <span class="tok-variableName">FirstName</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">set</span>; } <span class="tok-operator">=</span> <span class="tok-typeName">string</span>.<span class="tok-variableName">Empty</span>;</div><div class="cm-line">    <span class="tok-keyword">public</span> <span class="tok-typeName">string</span> <span class="tok-variableName">LastName</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">set</span>; } <span class="tok-operator">=</span> <span class="tok-typeName">string</span>.<span class="tok-variableName">Empty</span>;</div><div class="cm-line">    <span class="tok-keyword">public</span> <span class="tok-typeName">string</span> <span class="tok-variableName">Email</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">set</span>; } <span class="tok-operator">=</span> <span class="tok-typeName">string</span>.<span class="tok-variableName">Empty</span>;</div><div class="cm-line">    <span class="tok-keyword">public</span> <span class="tok-variableName">CustomerTier</span> <span class="tok-variableName">Tier</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">set</span>; }</div><div class="cm-line">}</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">public</span> <span class="tok-keyword">enum</span> <span class="tok-variableName">CustomerTier</span> { <span class="tok-variableName">Standard</span>, <span class="tok-variableName">Premium</span>, <span class="tok-variableName">Enterprise</span> }</div><div class="cm-line"></div><div class="cm-line">[<span class="tok-variableName">MapFrom</span>(<span class="tok-keyword">typeof</span>(<span class="tok-variableName">Customer</span>))]</div><div class="cm-line">[<span class="tok-variableName">MapProperty</span>(<span class="tok-variableName">nameof</span>(<span class="tok-variableName">Customer</span>.<span class="tok-variableName">FirstName</span>), <span class="tok-variableName">nameof</span>(<span class="tok-variableName">CustomerDto</span>.<span class="tok-variableName">Name</span>))]</div><div class="cm-line"><span class="tok-keyword">public</span> <span class="tok-keyword">partial</span> <span class="tok-keyword">record</span> <span class="tok-variableName tok-definition">CustomerDto</span>(<span class="tok-typeName">int</span> <span class="tok-variableName">Id</span>, <span class="tok-typeName">string</span> <span class="tok-variableName">Name</span>, <span class="tok-typeName">string</span> <span class="tok-variableName">Email</span>, <span class="tok-typeName">string</span> <span class="tok-variableName">Tier</span>);</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Here&nbsp;<code>FirstName</code>&nbsp;on the source gets mapped to&nbsp;<code>Name</code>&nbsp;on the DTO. The generator handles it at build time — zero surprises at runtime.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f310.png" alt="🌐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Minimal API — Full Example</h2>



<p class="wp-block-paragraph">Here&#8217;s where it gets fun. Let&#8217;s use AotMapper in an ASP.NET Core Minimal API.</p>



<p class="wp-block-paragraph">First, add the ASP.NET Core integration:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code><div class="cm-line">dotnet add package ElBruno.AotMapper.AspNetCore</div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Then wire it up:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">MinimalApiSample</span>.<span class="tok-variableName">Models</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">AotMapper</span>.<span class="tok-variableName">AspNetCore</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">builder</span> <span class="tok-operator">=</span> <span class="tok-variableName">WebApplication</span>.<span class="tok-variableName">CreateBuilder</span>(<span class="tok-variableName">args</span>);</div><div class="cm-line"><span class="tok-variableName">builder</span>.<span class="tok-variableName">Services</span>.<span class="tok-variableName">AddAotMapper</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">app</span> <span class="tok-operator">=</span> <span class="tok-variableName">builder</span>.<span class="tok-variableName">Build</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">customers</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">List</span><span class="tok-operator">&lt;</span><span class="tok-variableName">Customer</span><span class="tok-operator">&gt;</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">new</span>() { <span class="tok-variableName">Id</span> <span class="tok-operator">=</span> <span class="tok-number">1</span>, <span class="tok-variableName">FirstName</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;Bruno&quot;</span>, <span class="tok-variableName">LastName</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;Capuano&quot;</span>,</div><div class="cm-line">            <span class="tok-variableName">Email</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;bruno@example.com&quot;</span>, <span class="tok-variableName">Tier</span> <span class="tok-operator">=</span> <span class="tok-variableName">CustomerTier</span>.<span class="tok-variableName">Enterprise</span> },</div><div class="cm-line">    <span class="tok-keyword">new</span>() { <span class="tok-variableName">Id</span> <span class="tok-operator">=</span> <span class="tok-number">2</span>, <span class="tok-variableName">FirstName</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;Jane&quot;</span>, <span class="tok-variableName">LastName</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;Smith&quot;</span>,</div><div class="cm-line">            <span class="tok-variableName">Email</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;jane@example.com&quot;</span>, <span class="tok-variableName">Tier</span> <span class="tok-operator">=</span> <span class="tok-variableName">CustomerTier</span>.<span class="tok-variableName">Premium</span> }</div><div class="cm-line">};</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">app</span>.<span class="tok-variableName">MapGet</span>(<span class="tok-string">&quot;/customers&quot;</span>, () <span class="tok-operator">=&gt;</span> <span class="tok-variableName">customers</span>.<span class="tok-variableName">Select</span>(<span class="tok-variableName">c</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">c</span>.<span class="tok-variableName">ToCustomerDto</span>()));</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">app</span>.<span class="tok-variableName">MapGet</span>(<span class="tok-string">&quot;/customers/{id}&quot;</span>, (<span class="tok-typeName">int</span> <span class="tok-variableName">id</span>) <span class="tok-operator">=&gt;</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">customer</span> <span class="tok-operator">=</span> <span class="tok-variableName">customers</span>.<span class="tok-variableName">FirstOrDefault</span>(<span class="tok-variableName">c</span> <span class="tok-operator">=&gt;</span> <span class="tok-variableName">c</span>.<span class="tok-variableName">Id</span> <span class="tok-operator">==</span> <span class="tok-variableName">id</span>);</div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">customer</span> <span class="tok-keyword">is</span> <span class="tok-atom">null</span> <span class="tok-operator">?</span> <span class="tok-variableName">Results</span>.<span class="tok-variableName">NotFound</span>() : <span class="tok-variableName">Results</span>.<span class="tok-variableName">Ok</span>(<span class="tok-variableName">customer</span>.<span class="tok-variableName">ToCustomerDto</span>());</div><div class="cm-line">});</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">app</span>.<span class="tok-variableName">Run</span>();</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">All the magic in the endpoing handlers with &gt; <code>c.ToCustomerDto()</code> (that&#8217;s the generated extension method)</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f5c4.png" alt="🗄" class="wp-smiley" style="height: 1em; max-height: 1em;" /> EF Core — In-Memory Mapping and SQL Projections</h2>



<p class="wp-block-paragraph">If you&#8217;re using Entity Framework, there&#8217;s a package for that, and I have some samples in the repo (I think I can do more here &#8230; I didn&#8217;t have time yet for this) </p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> More Attributes You Should Know</h2>



<p class="wp-block-paragraph">The library has a few more tricks:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Attribute</th><th class="has-text-align-left" data-align="left">What it does</th></tr></thead><tbody><tr><td class="has-text-align-left" data-align="left"><code>[MapFrom(typeof(T))]</code></td><td class="has-text-align-left" data-align="left">Map&nbsp;<strong>from</strong>&nbsp;a source type to this DTO</td></tr><tr><td class="has-text-align-left" data-align="left"><code>[MapTo(typeof(T))]</code></td><td class="has-text-align-left" data-align="left">Map&nbsp;<strong>to</strong>&nbsp;a destination type from this source</td></tr><tr><td class="has-text-align-left" data-align="left"><code>[MapProperty("src", "dest")]</code></td><td class="has-text-align-left" data-align="left">Remap a property with a different name</td></tr><tr><td class="has-text-align-left" data-align="left"><code>[MapIgnore]</code></td><td class="has-text-align-left" data-align="left">Skip a property during mapping</td></tr><tr><td class="has-text-align-left" data-align="left"><code>[MapConverter(typeof(T))]</code></td><td class="has-text-align-left" data-align="left">Use a custom converter for a property</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">And for custom converters, you implement&nbsp;<code>IMapConverter&lt;TSource, TDestination&gt;</code>:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-variableName tok-definition">UpperCaseConverter</span> : <span class="tok-variableName">IMapConverter</span><span class="tok-operator">&lt;</span><span class="tok-typeName">string</span>, <span class="tok-typeName">string</span><span class="tok-operator">&gt;</span></div><div class="cm-line">{</div><div class="cm-line">    <span class="tok-keyword">public</span> <span class="tok-typeName">string</span> <span class="tok-variableName">Convert</span>(<span class="tok-typeName">string</span> <span class="tok-variableName">source</span>) <span class="tok-operator">=&gt;</span> <span class="tok-variableName">source</span>.<span class="tok-variableName">ToUpperInvariant</span>();</div><div class="cm-line">}</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f3d7.png" alt="🏗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> How It Works Under the Hood</h2>



<p class="wp-block-paragraph">The flow is simple:</p>



<ol class="wp-block-list">
<li>You add <code>[MapFrom]</code> (or <code>[MapTo]</code>) to your DTO</li>



<li>During build, the Roslyn incremental source generator kicks in</li>



<li>It matches source and destination properties by name and type</li>



<li>It generates strongly-typed extension methods (like <code>.ToProductDto()</code>)</li>



<li>If something doesn&#8217;t match, you get a <strong>compile-time error</strong> — not a runtime surprise</li>
</ol>



<p class="wp-block-paragraph">That&#8217;s the whole point:&nbsp;<strong>move the errors to build time, move the mapping to generated code, and keep the runtime clean.</strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4c2.png" alt="📂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Samples</h2>



<p class="wp-block-paragraph">The repo includes three complete samples you can run right now:</p>



<ul class="wp-block-list">
<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f527.png" alt="🔧" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong><a href="https://github.com/elbruno/ElBruno.AotMapper/tree/main/src/samples/NativeAotSample">NativeAotSample</a></strong> — Basic mapping in a NativeAOT console app</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f310.png" alt="🌐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong><a href="https://github.com/elbruno/ElBruno.AotMapper/tree/main/src/samples/MinimalApiSample">MinimalApiSample</a></strong> — ASP.NET Core Minimal API with DI</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f5c4.png" alt="🗄" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong><a href="https://github.com/elbruno/ElBruno.AotMapper/tree/main/src/samples/EfProjectionSample">EfProjectionSample</a></strong> — EF Core with in-memory and SQL projections</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Resources</h2>



<ul class="wp-block-list">
<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4e6.png" alt="📦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>NuGet</strong>: <a href="https://www.nuget.org/packages/ElBruno.AotMapper">ElBruno.AotMapper</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f419.png" alt="🐙" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>GitHub</strong>: <a href="https://github.com/elbruno/ElBruno.AotMapper">github.com/elbruno/ElBruno.AotMapper</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4d6.png" alt="📖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Docs</strong>: <a href="https://file+.vscode-resource.vscode-cdn.net/c%3A/src/ElBruno.AotMapper/docs/quick-start.md">Quick Start</a> | <a href="https://file+.vscode-resource.vscode-cdn.net/c%3A/src/ElBruno.AotMapper/docs/supported-mappings.md">Supported Mappings</a> | <a href="https://file+.vscode-resource.vscode-cdn.net/c%3A/src/ElBruno.AotMapper/docs/ef-integration.md">EF Integration</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4dc.png" alt="📜" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>License</strong>: MIT</li>
</ul>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/04/16/%f0%9f%97%ba%ef%b8%8f-aot-friendly-dto-mapping-in-net/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40228</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/04/gemini_generated_image_7a9gt7a9gt7a9gt7.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/gemini_generated_image_7a9gt7a9gt7a9gt7.png">
			<media:title type="html">Gemini_Generated_Image_7a9gt7a9gt7a9gt7</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/gemini_generated_image_7a9gt7a9gt7a9gt7.png?w=1024"/>
	</item>
		<item>
		<title>&#128444;️ MAI-Image-2 Just Dropped — And .NET Support Is Already Here</title>
		<link>https://elbruno.com/2026/04/13/%f0%9f%96%bc%ef%b8%8f-mai-image-2-just-dropped-and-net-support-is-already-here/</link>
					<comments>https://elbruno.com/2026/04/13/%f0%9f%96%bc%ef%b8%8f-mai-image-2-just-dropped-and-net-support-is-already-here/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Mon, 13 Apr 2026 14:39:00 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Image Generation]]></category>
		<category><![CDATA[NuGet]]></category>
		<category><![CDATA[NuGet Packages]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=40219</guid>

					<description><![CDATA[Hi! When Microsoft announced&#160;MAI-Image-2, I immediately thought:&#160;&#8220;I need to add this to ElBruno.Text2Image. Today.&#8221; So I did. 😄 MAI-Image-2 is Microsoft&#8217;s new image generation model on Microsoft Foundry — high-quality generation, a synchronous API (no polling!), a 32K character prompt limit, and flexible dimensions. And it&#8217;s already supported in ElBruno.Text2Image with the same clean interface you already know. Let me [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">When Microsoft announced&nbsp;<a href="https://microsoft.ai/news/introducing-MAI-Image-2/">MAI-Image-2</a>, I immediately thought:&nbsp;<em>&#8220;I need to add this to ElBruno.Text2Image. Today.&#8221;</em></p>



<p class="wp-block-paragraph">So I did. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f604.png" alt="😄" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph"><strong>MAI-Image-2</strong> is Microsoft&#8217;s new image generation model on Microsoft Foundry — high-quality generation, a <strong>synchronous API</strong> (no polling!), a 32K character prompt limit, and flexible dimensions. And it&#8217;s already supported in <strong>ElBruno.Text2Image</strong> with the same clean interface you already know.</p>



<p class="wp-block-paragraph">Let me show you how it works.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2601.png" alt="☁" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Getting Started — MAI-Image-2 on Azure AI Foundry</h2>



<p class="wp-block-paragraph">MAI-Image-2 delivers high-quality image generation with a simpler developer experience than FLUX.2. The API is synchronous — you send a request, you get an image back. No 202 status codes, no polling loops, no waiting callbacks. Just a prompt and a picture.</p>



<p class="wp-block-paragraph">Here&#8217;s all you need:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">Text2Image</span>;</div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-variableName">ElBruno</span>.<span class="tok-variableName">Text2Image</span>.<span class="tok-variableName">Foundry</span>;</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">generator</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">MaiImage2Generator</span>(</div><div class="cm-line">    <span class="tok-variableName">endpoint</span>: <span class="tok-string">&quot;https://your-resource.services.ai.azure.com&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">apiKey</span>: <span class="tok-string">&quot;your-api-key&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">modelId</span>: <span class="tok-string">&quot;MAI-Image-2&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">result</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">generator</span>.<span class="tok-variableName">GenerateAsync</span>(</div><div class="cm-line">    <span class="tok-string">&quot;a futuristic cityscape with neon lights, cyberpunk style&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">await</span> <span class="tok-variableName">result</span>.<span class="tok-variableName">SaveAsync</span>(<span class="tok-string">&quot;mai-image2-output.png&quot;</span>);</div><div class="cm-line"><span class="tok-variableName">Console</span>.<span class="tok-variableName">WriteLine</span>(<span class="tok-variableName">$</span><span class="tok-string">&quot;Generated in {result.InferenceTimeMs}ms&quot;</span>);</div></code></pre>
		</div>
	</div>
</div>


<h3 class="wp-block-heading">Setting up credentials</h3>



<p class="wp-block-paragraph">The library reads from User Secrets, environment variables, or&nbsp;<code>appsettings.json</code>. For local development:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-powershell"><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">user-secrets</span> <span class="tok-variableName">set</span> <span class="tok-variableName">MAI_IMAGE2_ENDPOINT</span> <span class="tok-string">&quot;https://your-resource.services.ai.azure.com&quot;</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">user-secrets</span> <span class="tok-variableName">set</span> <span class="tok-variableName">MAI_IMAGE2_API_KEY</span> <span class="tok-string">&quot;your-api-key-here&quot;</span></div><div class="cm-line"><span class="tok-variableName">dotnet</span> <span class="tok-variableName">user-secrets</span> <span class="tok-variableName">set</span> <span class="tok-variableName">MAI_IMAGE2_MODEL_ID</span> <span class="tok-string">&quot;MAI-Image-2&quot;</span></div></code></pre>
		</div>
	</div>
</div>


<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" />&nbsp;<strong>Fun fact:</strong>&nbsp;MAI-Image-2 uses a dedicated&nbsp;<code>/mai/v1/images/generations</code>&nbsp;endpoint. The library handles this automatically — just provide your&nbsp;<code>.services.ai.azure.com</code>&nbsp;base URL and it builds the correct API path for you.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a1.png" alt="⚡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Key Differences from FLUX.2</h2>



<p class="wp-block-paragraph">If you&#8217;re already using FLUX.2 with this library, here&#8217;s how MAI-Image-2 compares:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Feature</th><th class="has-text-align-left" data-align="left">MAI-Image-2</th><th class="has-text-align-left" data-align="left">FLUX.2</th></tr></thead><tbody><tr><td class="has-text-align-left" data-align="left"><strong>API style</strong></td><td class="has-text-align-left" data-align="left">Synchronous (direct response)</td><td class="has-text-align-left" data-align="left">Asynchronous (202 + polling)</td></tr><tr><td class="has-text-align-left" data-align="left"><strong>API path</strong></td><td class="has-text-align-left" data-align="left"><code>/mai/v1/images/generations</code></td><td class="has-text-align-left" data-align="left">BFL provider path</td></tr><tr><td class="has-text-align-left" data-align="left"><strong>Prompt limit</strong></td><td class="has-text-align-left" data-align="left">32,000 characters</td><td class="has-text-align-left" data-align="left">~1,000 characters</td></tr><tr><td class="has-text-align-left" data-align="left"><strong>Min dimensions</strong></td><td class="has-text-align-left" data-align="left">768px (per side)</td><td class="has-text-align-left" data-align="left">256px</td></tr><tr><td class="has-text-align-left" data-align="left"><strong>Max dimensions</strong></td><td class="has-text-align-left" data-align="left">1M total pixels</td><td class="has-text-align-left" data-align="left">Model-dependent</td></tr><tr><td class="has-text-align-left" data-align="left"><strong>Interface</strong></td><td class="has-text-align-left" data-align="left"><code>IImageGenerator</code></td><td class="has-text-align-left" data-align="left"><code>IImageGenerator</code></td></tr><tr><td class="has-text-align-left" data-align="left"><strong>DI support</strong></td><td class="has-text-align-left" data-align="left"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Same pattern</td><td class="has-text-align-left" data-align="left"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Same pattern</td></tr><tr><td class="has-text-align-left" data-align="left"><strong>Endpoint auto-conversion</strong></td><td class="has-text-align-left" data-align="left"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td class="has-text-align-left" data-align="left"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The synchronous API is a big deal for developer experience. No more writing polling loops or handling intermediate states. Send a prompt, get an image. Done.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f50c.png" alt="🔌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Same Interface, Multiple Backends</h2>



<p class="wp-block-paragraph"><strong><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Microsoft.Extensions.AI Compatible</strong></p>



<p class="wp-block-paragraph">Every generator in the library — including the new MaiImage2Generator — implements the standard Microsoft.Extensions.AI.IImageGenerator interface from the<br />Microsoft.Extensions.AI.Abstractions package. This means you can use ElBruno.Text2Image as a drop-in provider anywhere the MEAI abstraction is expected — dependency<br />injection, middleware pipelines, or any framework that programs against IImageGenerator. Cloud or local, FLUX.2 or MAI-Image-2 or Stable Diffusion — they all plug<br />into the same standard .NET AI contract.</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-comment">// MAI-Image-2 (cloud)</span></div><div class="cm-line"><span class="tok-variableName">IImageGenerator</span> <span class="tok-variableName">generator</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">MaiImage2Generator</span>(<span class="tok-variableName">endpoint</span>, <span class="tok-variableName">apiKey</span>, <span class="tok-variableName">modelId</span>: <span class="tok-string">&quot;MAI-Image-2&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-comment">// FLUX.2 Pro (cloud)</span></div><div class="cm-line"><span class="tok-variableName">IImageGenerator</span> <span class="tok-variableName">generator</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">Flux2Generator</span>(<span class="tok-variableName">endpoint</span>, <span class="tok-variableName">apiKey</span>, <span class="tok-variableName">modelId</span>: <span class="tok-string">&quot;FLUX.2-pro&quot;</span>);</div><div class="cm-line"></div><div class="cm-line"><span class="tok-comment">// Stable Diffusion 1.5 (local)</span></div><div class="cm-line"><span class="tok-variableName">IImageGenerator</span> <span class="tok-variableName">generator</span> <span class="tok-operator">=</span> <span class="tok-keyword">new</span> <span class="tok-variableName">StableDiffusion15</span>();</div><div class="cm-line"></div><div class="cm-line"><span class="tok-comment">// Same API for all three</span></div><div class="cm-line"><span class="tok-keyword">var</span> <span class="tok-variableName tok-definition">result</span> <span class="tok-operator">=</span> <span class="tok-keyword">await</span> <span class="tok-variableName">generator</span>.<span class="tok-variableName">GenerateAsync</span>(<span class="tok-string">&quot;a beautiful landscape&quot;</span>);</div></code></pre>
		</div>
	</div>
</div>


<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f489.png" alt="💉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Dependency Injection</h2>



<p class="wp-block-paragraph">If you&#8217;re building with DI, the library has an extension method ready to go:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-c#"><div class="cm-line"><span class="tok-variableName">services</span>.<span class="tok-variableName">AddMaiImage2Generator</span>(</div><div class="cm-line">    <span class="tok-variableName">endpoint</span>: <span class="tok-string">&quot;https://your-resource.services.ai.azure.com&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">apiKey</span>: <span class="tok-string">&quot;your-api-key&quot;</span>,</div><div class="cm-line">    <span class="tok-variableName">modelId</span>: <span class="tok-string">&quot;MAI-Image-2&quot;</span>);</div><div class="cm-line"></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Same pattern as the FLUX.2 registration. Inject&nbsp;<code>IImageGenerator</code>&nbsp;and you&#8217;re done.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f517.png" alt="🔗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Links</h2>



<ul class="wp-block-list">
<li><strong>Repository:</strong> <a href="https://github.com/elbruno/ElBruno.Text2Image">github.com/elbruno/ElBruno.Text2Image</a></li>



<li><strong>NuGet:</strong> <a href="https://www.nuget.org/packages/ElBruno.Text2Image.Foundry">nuget.org/packages/ElBruno.Text2Image.Foundry</a></li>



<li><strong>MAI-Image-2 Announcement:</strong> <a href="https://microsoft.ai/news/introducing-MAI-Image-2/">Introducing MAI-Image-2</a></li>



<li><strong>Setup Guide:</strong> <a href="https://github.com/elbruno/ElBruno.Text2Image/blob/main/docs/mai-image-2-setup-guide.md">MAI-Image-2 Setup Guide</a></li>



<li><strong>v0.8.0 Release:</strong> <a href="https://github.com/elbruno/ElBruno.Text2Image/releases/tag/v0.8.0">github.com/elbruno/ElBruno.Text2Image/releases/tag/v0.8.0</a></li>
</ul>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/04/13/%f0%9f%96%bc%ef%b8%8f-mai-image-2-just-dropped-and-net-support-is-already-here/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">40219</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/04/image.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/image.png">
			<media:title type="html">image</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>
	</item>
		<item>
		<title>&#128640; Bing Open-Sources New Embedding Models — Now Available in C# with LocalEmbeddings.Harrier</title>
		<link>https://elbruno.com/2026/04/08/%f0%9f%9a%80-bing-open-sources-new-embedding-models-now-available-in-c-with-localembeddings-harrier/</link>
					<comments>https://elbruno.com/2026/04/08/%f0%9f%9a%80-bing-open-sources-new-embedding-models-now-available-in-c-with-localembeddings-harrier/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Wed, 08 Apr 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[Embeddings]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[NuGet]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=39431</guid>

					<description><![CDATA[The Microsoft Bing Search team just dropped something big: 👉 https://blogs.bing.com/search/April-2026/Microsoft-Open-Sources-Industry-Leading-Embedding-Model They’ve open-sourced a new set of high-quality embedding models, designed to be competitive with state-of-the-art approaches — and ready to run in modern AI workflows. And here’s the fun part 👇 You can already use these models from C#/.NET. 🤖 From Announcement to .NET [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">The Microsoft Bing Search team just dropped something big:</p>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://blogs.bing.com/search/April-2026/Microsoft-Open-Sources-Industry-Leading-Embedding-Model" target="_blank" rel="noreferrer noopener">https://blogs.bing.com/search/April-2026/Microsoft-Open-Sources-Industry-Leading-Embedding-Model</a></p>



<p class="wp-block-paragraph">They’ve <strong>open-sourced a new set of high-quality embedding models</strong>, designed to be competitive with state-of-the-art approaches — and ready to run in modern AI workflows.</p>



<p class="wp-block-paragraph">And here’s the fun part <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f447.png" alt="👇" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph"><strong>You can already use these models from C#/.NET.</strong></p>



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> From Announcement to .NET in Minutes</h2>



<p class="wp-block-paragraph">I integrated these new models into a C# library:</p>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://www.nuget.org/packages/ElBruno.LocalEmbeddings.Harrier">https://www.nuget.org/packages/ElBruno.LocalEmbeddings.Harrier</a></p>



<p class="wp-block-paragraph">Now you can:</p>



<ul class="wp-block-list">
<li>Run embeddings locally</li>



<li>Use Bing’s open models</li>



<li>Plug into your .NET apps</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f30d.png" alt="🌍" class="wp-smiley" style="height: 1em; max-height: 1em;" /> A Personal Take: Multilingual FTW</h2>



<p class="wp-block-paragraph">Harrier shines with <strong>multilingual support</strong>.</p>



<p class="wp-block-paragraph">You can embed content like:</p>



<ul class="wp-block-list">
<li>&#8220;Hello world!&#8221;</li>



<li>&#8220;Hola mundo!&#8221;</li>
</ul>



<p class="wp-block-paragraph">…and still get meaningful semantic relationships — out of the box.</p>



<p class="wp-block-paragraph">Perfect for global-ready AI apps <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f30e.png" alt="🌎" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> .NET + AI Ecosystem</h2>



<p class="wp-block-paragraph">You can combine this with:</p>



<ul class="wp-block-list">
<li>Foundry Local or Ollama for local AI</li>



<li>Microsoft Foundry for cloud</li>



<li>Microsoft Agent Framework for agents</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f6e0.png" alt="🛠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Getting Started</h2>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://www.nuget.org/packages/ElBruno.LocalEmbeddings.Harrier">https://www.nuget.org/packages/ElBruno.LocalEmbeddings.Harrier</a></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using ElBruno.LocalEmbeddings.Harrier;

await using var generator = await HarrierEmbeddingGenerator.CreateAsync();

var embeddings = await generator.GenerateAsync(&#91;"Hello world!", "Hola mundo!"]);
Console.WriteLine($"Dimensions: {embeddings&#91;0].Vector.Length}"); // 640
</pre></div>


<p class="wp-block-paragraph">Samples:</p>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://github.com/elbruno/elbruno.localembeddings/tree/main/samples/HarrierConsoleApp">https://github.com/elbruno/elbruno.localembeddings/tree/main/samples/HarrierConsoleApp</a><br /><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://github.com/elbruno/elbruno.localembeddings/tree/main/samples/HarrierMultilingualSample">https://github.com/elbruno/elbruno.localembeddings/tree/main/samples/HarrierMultilingualSample</a></p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/04/08/%f0%9f%9a%80-bing-open-sources-new-embedding-models-now-available-in-c-with-localembeddings-harrier/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">39431</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/02/small.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/02/small.png">
			<media:title type="html">small</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>
	</item>
		<item>
		<title>I Built a .NET 10 Knowledge Graph Builder (Inspired by Karpathy)</title>
		<link>https://elbruno.com/2026/04/06/i-built-a-net-10-knowledge-graph-builder-inspired-by-karpathy/</link>
					<comments>https://elbruno.com/2026/04/06/i-built-a-net-10-knowledge-graph-builder-inspired-by-karpathy/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Tue, 07 Apr 2026 00:05:06 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[GenAI]]></category>
		<category><![CDATA[GitHub]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[LLMs]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=39425</guid>

					<description><![CDATA[Hi! Earlier this year,&#160;Andrej Karpathy tweeted about using LLMs as &#8220;knowledge compilers&#8221;&#160;— a mind-bending idea: instead of asking an LLM questions, feed it raw data (papers, code, images) and let it automatically build a structured, navigable knowledge base. No RAG. No vector databases. Just pure understanding compiled into a graph. Then I saw&#160;@socialwithaayan showcase graphify&#160;— [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" width="704" height="384" src="https://elbruno.com/wp-content/uploads/2026/04/bp01_small.png?w=704" alt="" class="wp-image-39428" srcset="https://elbruno.com/wp-content/uploads/2026/04/bp01_small.png 704w, https://elbruno.com/wp-content/uploads/2026/04/bp01_small.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/04/bp01_small.png?w=300 300w" sizes="auto, (max-width: 704px) 100vw, 704px" /></figure>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">Earlier this year,&nbsp;<a href="https://x.com/karpathy/status/2039805659525644595">Andrej Karpathy tweeted about using LLMs as &#8220;knowledge compilers&#8221;</a>&nbsp;— a mind-bending idea: instead of asking an LLM questions, feed it raw data (papers, code, images) and let it automatically build a structured, navigable knowledge base. No RAG. No vector databases. Just pure understanding compiled into a graph.</p>



<p class="wp-block-paragraph">Then I saw&nbsp;<a href="https://x.com/socialwithaayan/status/2041192946369007924">@socialwithaayan showcase graphify</a>&nbsp;— a Python tool that does exactly this for codebases. It reads your source code, extracts relationships, builds a knowledge graph, and exports it as an interactive visualization, Obsidian vault, Neo4j script, or JSON.</p>



<p class="wp-block-paragraph">I thought:&nbsp;<em>&#8220;This needs to exist in .NET.&#8221;</em></p>



<p class="wp-block-paragraph">So I built&nbsp;<a href="https://github.com/elbruno/graphify-dotnet"><strong>graphify-dotnet</strong></a>.</p>



<h2 class="wp-block-heading">What is graphify-dotnet?<a href="https://github.com/elbruno/graphify-dotnet/blob/main/docs/blog-post.md#what-is-graphify-dotnet"></a></h2>



<p class="wp-block-paragraph">graphify-dotnet is a .NET 10 tool that reads your codebase — all those files scattered across folders, all that implicit structure hidden in class hierarchies and imports — and transforms it into a&nbsp;<strong>visual knowledge graph</strong>. No more navigating by keyword search. Instead, you see the actual structure: which classes talk to each other, which modules form natural clusters, which functions are the &#8220;god nodes&#8221; that everything depends on.</p>



<p class="wp-block-paragraph">It&#8217;s a multi-stage pipeline:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: coldfusion; title: ; notranslate">
┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│  Detect  │ -&gt; │ Extract  │ -&gt; │  Build   │ -&gt; │ Cluster  │
│  Files   │    │ Features │    │  Graph   │    │ (Louvain)│
└──────────┘    └──────────┘    └──────────┘    └──────────┘
                                                     │
                                                     v
┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│  Export  │ &lt;- │  Report  │ &lt;- │ Analyze  │ &lt;- │ Clustered│
│ Formats  │    │Generator │    │  Graph   │    │  Graph   │
└──────────┘    └──────────┘    └──────────┘    └──────────┘
</pre></div>


<p class="wp-block-paragraph"><strong>Scan → Extract relationships → Build the graph → Find communities → Analyze structure → Export to multiple formats.</strong></p>



<h2 class="wp-block-heading">Let&#8217;s See It in Action<a href="https://github.com/elbruno/graphify-dotnet/blob/main/docs/blog-post.md#lets-see-it-in-action"></a></h2>



<p class="wp-block-paragraph">Here are some real commands you can run right now:</p>



<p class="wp-block-paragraph"><strong>Build a knowledge graph from your project:</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
dotnet run --project src/Graphify.Cli -- run .
</pre></div>


<p class="wp-block-paragraph"><strong>Query the graph for connections:</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
dotnet run --project src/Graphify.Cli -- query "what connects AuthService to Database?"
</pre></div>


<p class="wp-block-paragraph"><strong>Explain a node:</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
dotnet run --project src/Graphify.Cli -- explain "UserController"
</pre></div>


<p class="wp-block-paragraph"><strong>Export to multiple formats:</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
dotnet run --project src/Graphify.Cli -- export --format html
dotnet run --project src/Graphify.Cli -- export --format neo4j
</pre></div>


<p class="wp-block-paragraph">The HTML export gives you an interactive vis.js graph. Click nodes, search by concept, filter by community. The Neo4j export lets you load the entire graph into a real graph database.</p>



<h2 class="wp-block-heading">Key Features<a href="https://github.com/elbruno/graphify-dotnet/blob/main/docs/blog-post.md#key-features"></a></h2>



<ul class="wp-block-list">
<li><strong>Multi-language parsing</strong>: Python, TypeScript, JavaScript, Go, Rust, Java, C#, C++, and more via tree-sitter AST extraction</li>



<li><strong>Hybrid extraction</strong>: Deterministic AST parsing + AI semantic analysis for docs and images</li>



<li><strong>Graph clustering</strong>: Louvain community detection so you can see natural groupings in your architecture</li>



<li><strong>Confidence tracking</strong>: Every relationship tagged as EXTRACTED, INFERRED, or AMBIGUOUS</li>



<li><strong>Multiple export formats</strong>: JSON, HTML, SVG, GraphML, Wiki, Obsidian vault, Neo4j Cypher</li>



<li><strong>SHA256 caching</strong>: Skip unchanged files — incremental updates instead of full rebuilds</li>



<li><strong>MCP server</strong>: Integrate with Claude, Copilot, and other AI assistants</li>



<li><strong>Multimodal</strong>: Handles code, Markdown, PDFs, and images (diagrams, screenshots, whiteboards)</li>
</ul>



<h2 class="wp-block-heading">Why This Matters<a href="https://github.com/elbruno/graphify-dotnet/blob/main/docs/blog-post.md#why-this-matters"></a></h2>



<p class="wp-block-paragraph">Most developers navigate codebases by searching: &#8220;Find all usages of X.&#8221; &#8220;Show me the inheritance tree.&#8221; We&#8217;re stuck with keyword queries because we don&#8217;t have a semantic map.</p>



<p class="wp-block-paragraph">A knowledge graph changes that. Suddenly you see the&nbsp;<em>shape</em>&nbsp;of your system. You spot the core dependencies, the bottlenecks, the modules that should talk but don&#8217;t. You understand the architecture at a glance.</p>



<p class="wp-block-paragraph">And because it&#8217;s built from LLM-powered extraction, the graph understands&nbsp;<em>meaning</em>, not just syntax. It sees that your&nbsp;<code>Repository</code>&nbsp;class is about data access. It connects concepts across files. It finds hidden relationships.</p>



<figure class="wp-block-image size-large"><img loading="lazy" width="704" height="384" src="https://elbruno.com/wp-content/uploads/2026/04/bp02_small.png?w=704" alt="" class="wp-image-39429" srcset="https://elbruno.com/wp-content/uploads/2026/04/bp02_small.png 704w, https://elbruno.com/wp-content/uploads/2026/04/bp02_small.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/04/bp02_small.png?w=300 300w" sizes="auto, (max-width: 704px) 100vw, 704px" /></figure>



<h2 class="wp-block-heading">Getting Started<a href="https://github.com/elbruno/graphify-dotnet/blob/main/docs/blog-post.md#getting-started"></a></h2>



<p class="wp-block-paragraph"><strong>Requirements</strong>: .NET 10 SDK</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
git clone https://github.com/elbruno/graphify-dotnet.git
cd graphify-dotnet

dotnet build graphify-dotnet.slnx

dotnet run --project src/Graphify.Cli -- run .
</pre></div>


<p class="wp-block-paragraph">That&#8217;s it. Run it on your own codebase and watch as your architecture unfolds.</p>



<h2 class="wp-block-heading">What&#8217;s Coming Next?<a href="https://github.com/elbruno/graphify-dotnet/blob/main/docs/blog-post.md#whats-coming-next"></a></h2>



<p class="wp-block-paragraph">The roadmap is ambitious. Based on Morpheus&#8217;s research, here&#8217;s what&#8217;s in the pipeline:</p>



<p class="wp-block-paragraph"><strong>Soon:</strong></p>



<ul class="wp-block-list">
<li><strong>Global dotnet tool</strong>: Install once with <code>dotnet tool install -g graphify</code>, then just run <code>graphify run .</code> anywhere</li>



<li><strong>Azure OpenAI support</strong>: Teams with Azure OpenAI deployments can now use graphify with those modelos</li>



<li><strong>Ollama / local models</strong>: Run entirely offline locally for privacy-sensitive code (healthcare, finance, defense)</li>



<li><strong>Watch mode</strong>: File watcher that increments the graph as you code, no full rebuilds needed</li>



<li><strong>Roslyn-powered C# extraction</strong>: Leverage .NET&#8217;s full compilation model for type-safe AST analysis (something the Python version can&#8217;t do)</li>
</ul>



<p class="wp-block-paragraph">I&#8217;m also exploring VS Code integration, Obsidian bidirectional sync, and cross-repository knowledge graphs.</p>



<h2 class="wp-block-heading">Star the Repo, Try It Out<a href="https://github.com/elbruno/graphify-dotnet/blob/main/docs/blog-post.md#star-the-repo-try-it-out"></a></h2>



<p class="wp-block-paragraph">This is open source, built for the .NET community.&nbsp;<a href="https://github.com/elbruno/graphify-dotnet">Head to GitHub and check it out.</a></p>



<p class="wp-block-paragraph">Clone it. Run it on your own projects. Build something with it. The pipeline is extensible — add custom extractors, export formats, or clustering algorithms.</p>



<p class="wp-block-paragraph">And if you find it useful, star the repo. Help us grow the graphify ecosystem in .NET.</p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/04/06/i-built-a-net-10-knowledge-graph-builder-inspired-by-karpathy/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">39425</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/04/bp01_small.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/bp01_small.png">
			<media:title type="html">bp01_small</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/bp01_small.png?w=704"/>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/bp02_small.png?w=704"/>
	</item>
		<item>
		<title>&#128221; Convert Anything to Markdown in .NET — Meet ElBruno.MarkItDotNet</title>
		<link>https://elbruno.com/2026/04/04/%f0%9f%93%9d-convert-anything-to-markdown-in-net-meet-elbruno-markitdotnet/</link>
					<comments>https://elbruno.com/2026/04/04/%f0%9f%93%9d-convert-anything-to-markdown-in-net-meet-elbruno-markitdotnet/#respond</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Sat, 04 Apr 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[MarkDown]]></category>
		<category><![CDATA[NuGet]]></category>
		<category><![CDATA[Open Source]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=39420</guid>

					<description><![CDATA[Hi! You know that feeling when you&#8217;re building an&#160;AI pipeline&#160;or a&#160;RAG workflow&#160;and you realize: &#8220;Wait… I need to turn all these PDFs, Word docs, HTML pages, and random files into something my LLM can actually eat&#8221;? 😅 Yeah, me too. That&#8217;s exactly why I built: 👉 ElBruno.MarkItDotNet A .NET library that converts files to clean Markdown. Think of [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<figure class="wp-block-image size-large"><img loading="lazy" width="1024" height="682" src="https://elbruno.com/wp-content/uploads/2026/04/designer_blog_16-9.png?w=1024" alt="" class="wp-image-39422" srcset="https://elbruno.com/wp-content/uploads/2026/04/designer_blog_16-9.png?w=1024 1024w, https://elbruno.com/wp-content/uploads/2026/04/designer_blog_16-9.png?w=150 150w, https://elbruno.com/wp-content/uploads/2026/04/designer_blog_16-9.png?w=300 300w, https://elbruno.com/wp-content/uploads/2026/04/designer_blog_16-9.png?w=768 768w, https://elbruno.com/wp-content/uploads/2026/04/designer_blog_16-9.png?w=1440 1440w, https://elbruno.com/wp-content/uploads/2026/04/designer_blog_16-9.png 1536w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">You know that feeling when you&#8217;re building an&nbsp;<strong>AI pipeline</strong>&nbsp;or a&nbsp;<strong>RAG workflow</strong>&nbsp;and you realize: &#8220;Wait… I need to turn all these PDFs, Word docs, HTML pages, and random files into something my LLM can actually eat&#8221;? <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">Yeah, me too. That&#8217;s exactly why I built:</p>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong><a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet" target="_blank" rel="noreferrer noopener">ElBruno.MarkItDotNet</a></strong></p>



<p class="wp-block-paragraph">A <strong>.NET library</strong> that converts files to clean Markdown. Think of it as the <strong>.NET version of Python&#8217;s markitdown</strong> — this one with dependency injection, streaming support, and a plugin architecture. Because we&#8217;re C# developers and we like our things in this way. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f60e.png" alt="😎" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a1.png" alt="⚡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Getting Started<a href="https://github.com/elbruno/ElBruno.MarkItDotNet/blob/main/docs/blog-post-markit-dotnet.md#-getting-started"></a></h2>



<p class="wp-block-paragraph">Install the NuGet package:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
dotnet add package ElBruno.MarkItDotNet
</pre></div>


<p class="wp-block-paragraph">And then… this is all you need:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using ElBruno.MarkItDotNet;

var converter = new MarkdownConverter();
var markdown = converter.ConvertToMarkdown("document.pdf");
Console.WriteLine(markdown);
</pre></div>


<p class="wp-block-paragraph">That&#8217;s it. PDF → Markdown. Done. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4c2.png" alt="📂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> What Can It Convert?<a href="https://github.com/elbruno/ElBruno.MarkItDotNet/blob/main/docs/blog-post-markit-dotnet.md#-what-can-it-convert"></a></h2>



<p class="wp-block-paragraph">Here&#8217;s where it gets fun. The core package supports&nbsp;<strong>12 file formats</strong>&nbsp;out of the box:</p>



<ul class="wp-block-list">
<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4c4.png" alt="📄" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Plain text</strong> (<code>.txt</code>, <code>.log</code>, <code>.md</code>)</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4cb.png" alt="📋" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>JSON</strong> — pretty-printed and fenced</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f310.png" alt="🌐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>HTML / HTM</strong> — strips tags, keeps content</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f517.png" alt="🔗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>URLs</strong> — fetches and converts web pages</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Word DOCX</strong> — headings, tables, links, images, footnotes</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4d5.png" alt="📕" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>PDF</strong> — word-level extraction with heading detection</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4ca.png" alt="📊" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>CSV / TSV</strong> — clean Markdown tables</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4e6.png" alt="📦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>XML</strong> — structured fenced blocks</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2699.png" alt="⚙" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>YAML / YML</strong> — fenced code blocks</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4f0.png" alt="📰" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>RTF</strong> — rich text to Markdown</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>EPUB</strong> — ebooks to Markdown</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f5bc.png" alt="🖼" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Images</strong> — <code>.jpg</code>, <code>.png</code>, <code>.gif</code>, <code>.bmp</code>, <code>.webp</code>, <code>.svg</code></li>
</ul>



<p class="wp-block-paragraph">And with the&nbsp;<strong>satellite packages</strong>, you get even more:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Package</th><th>What it does</th></tr></thead><tbody><tr><td><code>ElBruno.MarkItDotNet.Excel</code></td><td><code>.xlsx</code>&nbsp;spreadsheets → Markdown tables</td></tr><tr><td><code>ElBruno.MarkItDotNet.PowerPoint</code></td><td><code>.pptx</code>&nbsp;slides → Markdown with notes</td></tr><tr><td><code>ElBruno.MarkItDotNet.AI</code></td><td>AI-powered OCR, image captioning, audio transcription</td></tr><tr><td><code>ElBruno.MarkItDotNet.Whisper</code></td><td>Local audio transcription with Whisper (no API key!)</td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f9e0.png" alt="🧠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Stream It — Because Large Files Are Real<a href="https://github.com/elbruno/ElBruno.MarkItDotNet/blob/main/docs/blog-post-markit-dotnet.md#-stream-it--because-large-files-are-real"></a></h2>



<p class="wp-block-paragraph">One of the things I find that someone were requested was &gt;&gt; <strong>streaming API</strong>. When you&#8217;re processing a 500-page PDF, you don&#8217;t want to wait for the entire thing to load in memory. So:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using var stream = File.OpenRead("huge-document.pdf");

await foreach (var chunk in converter.ConvertStreamingAsync(stream, ".pdf"))
{
    Console.Write(chunk); // chunks arrive as they're processed
}
</pre></div>


<p class="wp-block-paragraph">This uses <code>IAsyncEnumerable&lt;string&gt;</code> — so it plays nicely with your async pipelines, web APIs, and real-time UIs. </p>



<p class="wp-block-paragraph">To be honest, I never faced this scenario before, but it really makes sense.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f489.png" alt="💉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Dependency Injection? Of Course<a href="https://github.com/elbruno/ElBruno.MarkItDotNet/blob/main/docs/blog-post-markit-dotnet.md#-dependency-injection-of-course"></a></h2>



<p class="wp-block-paragraph">If you&#8217;re building a real app (not just a console demo), you&#8217;ll want the DI registration:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
// Program.cs or Startup
services.AddMarkItDotNet();          // core converters
services.AddMarkItDotNetExcel();     // Excel support
services.AddMarkItDotNetPowerPoint(); // PowerPoint support
services.AddMarkItDotNetWhisper();   // local audio transcription
</pre></div>


<p class="wp-block-paragraph">Then inject <code>IMarkdownService</code> wherever you need it:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public class MyDocProcessor
{
    private readonly IMarkdownService _markdownService;

    public MyDocProcessor(IMarkdownService markdownService)
    {
        _markdownService = markdownService;
    }

    public async Task&lt;string&gt; ProcessAsync(Stream fileStream, string extension)
    {
        var result = await _markdownService.ConvertAsync(fileStream, extension);
        return result.Markdown;
    }
}
</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> AI-Powered Conversions<a href="https://github.com/elbruno/ElBruno.MarkItDotNet/blob/main/docs/blog-post-markit-dotnet.md#-ai-powered-conversions"></a></h2>



<p class="wp-block-paragraph">This is where things get really interesting. And thanks Copilot CLI for suggesting this <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f447.png" alt="👇" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">The <strong>ElBruno.MarkItDotNet.AI</strong> package uses <code>Microsoft.Extensions.AI</code> and an <code>IChatClient</code> to power:</p>



<ul class="wp-block-list">
<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f5bc.png" alt="🖼" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Image OCR &amp; captioning</strong> — describe what&#8217;s in an image</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4d5.png" alt="📕" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Scanned PDF enhancement</strong> — detects low-text pages and uses AI to extract content</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f399.png" alt="🎙" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Audio transcription</strong> — turn audio files into Markdown</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
services.AddMarkItDotNetAI(options =&gt;
{
    options.ImagePrompt = &quot;Describe this image in detail&quot;;
    options.AudioPrompt = &quot;Transcribe this audio&quot;;
});
</pre></div>


<pre class="wp-block-preformatted">Works with <strong>OpenAI</strong>, <strong>Azure OpenAI</strong>, or any <code>IChatClient</code> implementation. Your choice.</pre>



<p class="wp-block-paragraph">And if you want <strong>local audio transcription</strong> with zero cloud dependency? There&#8217;s <code>ElBruno.MarkItDotNet.Whisper</code> for that. </p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f517.png" alt="🔗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> URL to Markdown<a href="https://github.com/elbruno/ElBruno.MarkItDotNet/blob/main/docs/blog-post-markit-dotnet.md#-url-to-markdown"></a></h2>



<p class="wp-block-paragraph">One more thing my friend Hector suggested &gt;&gt; converting web pages:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
var service = new MarkdownService(registry);
var result = await service.ConvertUrlAsync("https://example.com");
Console.WriteLine(result.Markdown);
</pre></div>


<p class="wp-block-paragraph">Super handy for web scraping, research pipelines, or just saving articles as Markdown.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f50c.png" alt="🔌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Build Your Own Converters<a href="https://github.com/elbruno/ElBruno.MarkItDotNet/blob/main/docs/blog-post-markit-dotnet.md#-build-your-own-converters"></a></h2>



<p class="wp-block-paragraph">Don&#8217;t see your format? No problem. Implement <code>IMarkdownConverter</code> and plug it in:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public class MyCustomConverter : IMarkdownConverter
{
    public string&#91;] SupportedExtensions =&gt; &#91;&quot;.custom&quot;];

    public Task&lt;ConversionResult&gt; ConvertAsync(Stream stream, string extension)
    {
        // your conversion logic here
    }
}
</pre></div>


<p class="wp-block-paragraph">Or bundle multiple converters into a plugin with&nbsp;<code>IConverterPlugin</code>. The architecture is designed to be extended.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f3ae.png" alt="🎮" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 18 Sample Apps<a href="https://github.com/elbruno/ElBruno.MarkItDotNet/blob/main/docs/blog-post-markit-dotnet.md#-18-sample-apps"></a></h2>



<p class="wp-block-paragraph">Yes, <strong>18 samples</strong>. I went a bit overboard <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> (not me, Copilot, you know what I mean):</p>



<ul class="wp-block-list">
<li><strong>BasicConversion</strong> — text, JSON, HTML</li>



<li><strong>PdfConversion</strong> — PDF + streaming</li>



<li><strong>DocxConversion</strong> — Word documents</li>



<li><strong>ExcelConversion</strong> — spreadsheets</li>



<li><strong>PowerPointConversion</strong> — slides</li>



<li><strong>AiImageDescription</strong> — AI image analysis</li>



<li><strong>WhisperTranscription</strong> — local audio</li>



<li><strong>MarkItDotNet.WebApi</strong> — minimal API with uploads + SSE</li>



<li><strong>BatchProcessor</strong> — folder batch conversion</li>



<li><strong>RagPipeline</strong> — RAG ingestion pipeline</li>



<li>…and more!</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Final Thoughts<a href="https://github.com/elbruno/ElBruno.MarkItDotNet/blob/main/docs/blog-post-markit-dotnet.md#-final-thoughts"></a></h2>



<p class="wp-block-paragraph">This project started because I needed a <strong>clean, extensible way to convert files to Markdown in .NET</strong> — especially for AI workflows. Python had <code>markitdown</code>, but .NET didn&#8217;t have a good equivalent. So I built some pet projects and they were on my personal toolbox for a while. </p>



<p class="wp-block-paragraph">Then someone ask a question, and <a href="https://www.youtube.com/watch?v=lX1wtTgCXNs" target="_blank" rel="noreferrer noopener">put a Squad to package everything</a>.</p>



<p class="wp-block-paragraph">Currently supports <strong>15+ file formats</strong>, has <strong>streaming APIs</strong>, plays nice with <strong>dependency injection</strong>, and can even use <strong>AI for OCR and transcription</strong>. Plus, it&#8217;s open source and ready for your PRs. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>NuGet:</strong> <a href="https://www.nuget.org/packages/ElBruno.MarkItDotNet">ElBruno.MarkItDotNet</a> </p>



<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Repo:</strong> <a href="https://github.com/elbruno/ElBruno.MarkItDotNet">https://github.com/elbruno/ElBruno.MarkItDotNet</a></p>



<p class="wp-block-paragraph">If you try it, let me know what you build! <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f64c.png" alt="🙌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/04/04/%f0%9f%93%9d-convert-anything-to-markdown-in-net-meet-elbruno-markitdotnet/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">39420</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/04/designer_blog_16-9.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/designer_blog_16-9.png">
			<media:title type="html">Designer_blog_16-9</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>

		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/designer_blog_16-9.png?w=1024"/>
	</item>
		<item>
		<title>&#127775; Gemma 4 Is Here — And My C# Library Is (Almost) Ready</title>
		<link>https://elbruno.com/2026/04/03/%f0%9f%8c%9f-gemma-4-is-here-and-my-c-library-is-almost-ready/</link>
					<comments>https://elbruno.com/2026/04/03/%f0%9f%8c%9f-gemma-4-is-here-and-my-c-library-is-almost-ready/#comments</comments>
		
		<dc:creator><![CDATA[elbruno]]></dc:creator>
		<pubDate>Fri, 03 Apr 2026 21:05:08 +0000</pubDate>
				<category><![CDATA[EnglishPost]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Code Sample]]></category>
		<category><![CDATA[English Post]]></category>
		<category><![CDATA[Gemma]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[LocalLLMs]]></category>
		<category><![CDATA[NuGet]]></category>
		<guid isPermaLink="false">http://elbruno.com/?p=39413</guid>

					<description><![CDATA[Hi! So Google just dropped Gemma 4 — their most capable open model family yet — and I couldn&#8217;t resist. I spent a good chunk of time digging into the architecture, trying to convert models, hitting walls, finding workarounds, and hitting more walls. Here&#8217;s where things stand with ElBruno.LocalLLMs. Spoiler: the library is ready for Gemma 4. The ONNX runtime&#8230; [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <em>This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> in C# are 100% mine.</em></p>



<p class="wp-block-paragraph">Hi!</p>



<p class="wp-block-paragraph">So Google just dropped <strong>Gemma 4</strong> — their most capable open model family yet — and I couldn&#8217;t resist. I spent a good chunk of time digging into the architecture, trying to convert models, hitting walls, finding workarounds, and hitting more walls. Here&#8217;s where things stand with <strong><a href="https://www.nuget.org/packages/ElBruno.LocalLLMs" target="_blank" rel="noreferrer noopener">ElBruno.LocalLLMs</a></strong>.</p>



<p class="wp-block-paragraph">Spoiler: the library is <em>ready</em> for Gemma 4. The ONNX runtime&#8230; not yet. So, let me tell you the whole story.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Wait, What&#8217;s Gemma 4?<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-gemma4-support.md#wait-whats-gemma-4"></a></h2>



<p class="wp-block-paragraph">Google released four new models on April 2, 2026, and they&#8217;re pretty wild:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Model</th><th>Parameters</th><th>What&#8217;s Cool</th><th>Context</th></tr></thead><tbody><tr><td><strong>E2B IT</strong></td><td>5.1B (only 2.3B active!)</td><td>Tiny but punches above its weight</td><td>128K</td></tr><tr><td><strong>E4B IT</strong></td><td>8B (4.5B active)</td><td>Sweet spot for most use cases</td><td>128K</td></tr><tr><td><strong>26B A4B IT</strong></td><td>25.2B (3.8B active)</td><td>MoE — only fires 3.8B params per token <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f92f.png" alt="🤯" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td><td>256K</td></tr><tr><td><strong>31B IT</strong></td><td>30.7B</td><td>The big one, dense, no tricks</td><td>256K</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The magic sauce is something called&nbsp;<strong>Per-Layer Embeddings (PLE)</strong>&nbsp;— basically, each transformer layer gets its own little embedding input. That&#8217;s how a 5.1B model acts like a 2.3B one. Clever stuff.</p>



<p class="wp-block-paragraph">They&#8217;re all <strong>Apache 2.0</strong>. No gating, no license hoops. I like that.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">What I Got Working (v0.8.0)<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-gemma4-support.md#what-i-got-working-v080"></a></h2>



<h3 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Model Definitions — Done<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-gemma4-support.md#-model-definitions--done"></a></h3>



<p class="wp-block-paragraph">All four Gemma 4 variants are registered and ready to go:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
var options = new LocalLLMsOptions
{
    Model = KnownModels.Gemma4E2BIT  // Smallest, edge-optimized
};
</pre></div>


<p class="wp-block-paragraph">I added&nbsp;<code>Gemma4E2BIT</code>,&nbsp;<code>Gemma4E4BIT</code>,&nbsp;<code>Gemma4_26BA4BIT</code>, and&nbsp;<code>Gemma4_31BIT</code>. The moment ONNX models exist, you just point and shoot.</p>



<h3 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Chat Template — Already Works<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-gemma4-support.md#-chat-template--already-works"></a></h3>



<p class="wp-block-paragraph">Here&#8217;s the fun part: Gemma 4 uses the&nbsp;<strong>exact same chat template</strong>&nbsp;as Gemma 2 and 3:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-apl"><div class="cm-line"><span class="tok-variableName">&lt;</span><span class="tok-keyword">start_of_turn</span><span class="tok-variableName">&gt;</span><span class="tok-keyword">user</span></div><div class="cm-line"><span class="tok-keyword">What</span> <span class="tok-keyword">is</span> <span class="tok-keyword">the</span> <span class="tok-keyword">capital</span> <span class="tok-keyword">of</span> <span class="tok-keyword">France</span><span class="tok-variableName">?&lt;</span><span class="tok-keyword">end_of_turn</span><span class="tok-variableName">&gt;</span></div><div class="cm-line"><span class="tok-variableName">&lt;</span><span class="tok-keyword">start_of_turn</span><span class="tok-variableName">&gt;</span><span class="tok-keyword">model</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">My existing&nbsp;<code>GemmaFormatter</code>&nbsp;handles it perfectly. Zero code changes needed. System messages fold into the first user turn, tool calling works — the whole thing just&#8230; works. I love when that happens.</p>



<h3 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Tool Calling — Yep, That Too<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-gemma4-support.md#-tool-calling--yep-that-too"></a></h3>



<p class="wp-block-paragraph">Gemma 4 natively supports function calling, and my formatter already handles the Gemma tool-calling format with proper JSON function definitions. No changes needed.</p>



<h3 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Tests — A Lot of Them<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-gemma4-support.md#-tests--a-lot-of-them"></a></h3>



<p class="wp-block-paragraph">I went a bit overboard here (no regrets, and thanks Copilot!):</p>



<ul class="wp-block-list">
<li><strong>6 model definition tests</strong> — making sure all four variants are correctly registered</li>



<li><strong>9 tool-calling tests</strong> — validating function calling scenarios with Gemma 4</li>



<li><strong>195 multilingual tests</strong> — this one deserves its own section (see below)</li>
</ul>



<p class="wp-block-paragraph">All&nbsp;<strong>697 tests</strong>&nbsp;pass. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<h3 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Conversion Scripts — Ready and Waiting<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-gemma4-support.md#-conversion-scripts--ready-and-waiting"></a></h3>



<p class="wp-block-paragraph">I wrote dedicated Python and PowerShell conversion scripts:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
python scripts/convert_gemma4.py --model-size e2b --output-dir ./models/gemma4-e2b
</pre></div>


<p class="wp-block-paragraph">They&#8217;re ready. They just need a runtime that can handle Gemma 4. Which brings me to&#8230;</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/23f3.png" alt="⏳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> The Honest Part: ONNX Conversion Is Blocked <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f614.png" alt="😔" class="wp-smiley" style="height: 1em; max-height: 1em;" /><a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-gemma4-support.md#-the-honest-part-onnx-conversion-is-blocked"></a></h2>



<p class="wp-block-paragraph">OK, here&#8217;s where I hit a wall. <strong>The ONNX conversion doesn&#8217;t work yet.</strong> <br />( I maybe missing something here, but hey, it&#8217;s a long weekend !)</p>



<h3 class="wp-block-heading">What&#8217;s the Problem?<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-gemma4-support.md#whats-the-problem"></a></h3>



<p class="wp-block-paragraph">Gemma 4 has three architectural features that&nbsp;<code>onnxruntime-genai</code>&nbsp;v0.12.2 simply doesn&#8217;t support:</p>



<ol class="wp-block-list">
<li><strong>Per-Layer Embeddings (PLE)</strong> — each layer needs a separate <code>per_layer_inputs</code> tensor. The runtime expects one embedding output. Not three dozen.</li>



<li><strong>Variable Head Dimensions</strong> — sliding attention layers use <code>head_dim=256</code>, full attention layers (every 5th one) use <code>512</code>. The runtime config only has ONE <code>head_size</code> field. Pick one? Yeah, no.</li>



<li><strong>KV Cache Sharing</strong> — 35 layers share only 15 unique KV cache pairs. The runtime expects a 1:1 mapping. Math doesn&#8217;t math.</li>
</ol>



<h3 class="wp-block-heading">What I Tried (The Fun Part)<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-gemma4-support.md#what-i-tried-the-fun-part"></a></h3>



<p class="wp-block-paragraph">Here&#8217;s my adventure:</p>



<ul class="wp-block-list">
<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f527.png" alt="🔧" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Patched the GenAI builder</strong> to route Gemma 4 through the Gemma 3 pipeline — it actually produced a 1.6GB ONNX file! But then the runtime choked with a shape mismatch at the full attention layers. So close.</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f50d.png" alt="🔍" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Examined the onnx-community models</strong> — they have the right structure, but the I/O format is incompatible with GenAI&#8217;s KV cache management.</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f9ea.png" alt="🧪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Tried loading as <code>Gemma4ForCausalLM</code></strong> — nope, weights are stored under a multimodal prefix. Mismatch everywhere.</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f50e.png" alt="🔎" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Searched for pre-release builds</strong> — nothing. 0.12.2 is the latest.</li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4cb.png" alt="📋" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Checked GitHub issues/PRs</strong> — zero Gemma 4 mentions in the repo.</li>
</ul>



<h3 class="wp-block-heading">So When Will It Work?<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-gemma4-support.md#so-when-will-it-work"></a></h3>



<p class="wp-block-paragraph">The moment&nbsp;<code>onnxruntime-genai</code>&nbsp;adds Gemma 4 support, I&#8217;m ready to go:</p>



<ul class="wp-block-list">
<li>Model definitions <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>



<li>Chat template <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>



<li>Tests <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>



<li>Conversion scripts <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>



<li>Documentation <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>
</ul>



<p class="wp-block-paragraph">I&#8217;m watching: <a href="https://github.com/microsoft/onnxruntime-genai/releases" target="_blank" rel="noreferrer noopener">microsoft/onnxruntime-genai releases</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Bonus: I Went Multilingual<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-gemma4-support.md#bonus-i-went-multilingual"></a></h2>



<p class="wp-block-paragraph">While I was in testing mode, I figured — why not make sure all my formatters handle every language properly? So I added&nbsp;<strong>195 multilingual tests</strong>&nbsp;covering:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Script/Language</th><th>Examples</th></tr></thead><tbody><tr><td>CJK</td><td>日本語, 中文, 한국어</td></tr><tr><td>Cyrillic</td><td>Русский</td></tr><tr><td>Arabic</td><td>العربية (RTL)</td></tr><tr><td>Hebrew</td><td>עברית (RTL)</td></tr><tr><td>Devanagari</td><td>हिन्दी</td></tr><tr><td>Tamil</td><td>தமிழ்</td></tr><tr><td>Thai</td><td>ไทย</td></tr><tr><td>European</td><td>Ñ, Ü, Ø, Ž, Ą</td></tr><tr><td>Emoji</td><td><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" />, <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f44b.png" alt="👋" class="wp-smiley" style="height: 1em; max-height: 1em;" />, <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f30d.png" alt="🌍" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td></tr><tr><td>Zero-width</td><td>ZWJ, ZWNJ characters</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">All&nbsp;<strong>7 formatters</strong>&nbsp;(ChatML, Phi3, Llama3, Qwen, Mistral, Gemma, DeepSeek) handle Unicode correctly. If you&#8217;re running models locally, you probably care about this. I know I do.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Try It Out<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-gemma4-support.md#try-it-out"></a></h2>



<p class="wp-block-paragraph">Grab v0.8.0:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
dotnet add package ElBruno.LocalLLMs --version 0.8.0
</pre></div>


<p class="wp-block-paragraph">Gemma 4 ONNX models aren&#8217;t ready yet, but there are <strong>25+ other models</strong> that work right now:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using ElBruno.LocalLLMs;
using Microsoft.Extensions.AI;

// Gemma 2 works great today
var options = new LocalLLMsOptions { Model = KnownModels.Gemma2_2BIT };
using var client = await LocalChatClient.CreateAsync(options);
var response = await client.GetResponseAsync(&#91;
    new(ChatRole.User, "Tell me about Gemma 4!")
]);
Console.WriteLine(response.Text);
</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">Links<a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blog-gemma4-support.md#links"></a></h2>



<ul class="wp-block-list">
<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4e6.png" alt="📦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://www.nuget.org/packages/ElBruno.LocalLLMs">NuGet Package</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f4d6.png" alt="📖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/supported-models.md">Supported Models</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f527.png" alt="🔧" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/onnx-conversion.md">ONNX Conversion Guide</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f6ab.png" alt="🚫" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://github.com/elbruno/ElBruno.LocalLLMs/blob/main/docs/blocked-models.md">Blocked Models Reference</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f310.png" alt="🌐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://blog.google/innovation-and-ai/technology/developers-tools/gemma-4/">Google Gemma 4 Announcement</a></li>



<li><img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f419.png" alt="🐙" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://github.com/elbruno/ElBruno.LocalLLMs">GitHub Repository</a></li>
</ul>



<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph">Happy coding!</p>



<p class="wp-block-paragraph">Greetings</p>



<p class="wp-block-paragraph">El Bruno</p>



<p class="wp-block-paragraph">More posts in my blog <a rel="noreferrer noopener" href="https://www.elbruno.com" target="_blank">ElBruno.com</a>.</p>



<p class="wp-block-paragraph">More info in <a href="https://beacons.ai/elbruno" target="_blank" rel="noreferrer noopener">https://beacons.ai/elbruno</a></p>



<hr class="wp-block-separator has-css-opacity is-style-wide" />
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://elbruno.com/2026/04/03/%f0%9f%8c%9f-gemma-4-is-here-and-my-c-library-is-almost-ready/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">39413</post-id>
		<media:thumbnail url="https://elbruno.com/wp-content/uploads/2026/04/gemini_generated_image_emuwsbemuwsbemuw.png"/>
		<media:content medium="image" url="https://elbruno.com/wp-content/uploads/2026/04/gemini_generated_image_emuwsbemuwsbemuw.png">
			<media:title type="html">Gemini_Generated_Image_emuwsbemuwsbemuw</media:title>
		</media:content>

		<media:content medium="image" url="https://2.gravatar.com/avatar/261dbbb4696f8ffecc322124e5623f98dba032a99f5adcc99f8c2d6deb3ab2d3?s=96&amp;d=identicon&amp;r=G">
			<media:title type="html">brunocapuano</media:title>
		</media:content>
	</item>
	</channel>
</rss>