<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Devesh tiwari</title>
    <description>The latest articles on DEV Community by Devesh tiwari (@deveshtiw).</description>
    <link>https://dev.to/deveshtiw</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3837068%2F0d09d19f-9ffc-40a6-a246-0a04c941e204.jpeg</url>
      <title>DEV Community: Devesh tiwari</title>
      <link>https://dev.to/deveshtiw</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/deveshtiw"/>
    <language>en</language>
    <item>
      <title>I Built a Terminal AI Assistant That Never Freezes — Here's the Architecture</title>
      <dc:creator>Devesh tiwari</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:32:18 +0000</pubDate>
      <link>https://dev.to/deveshtiw/i-built-a-terminal-ai-assistant-that-never-freezes-heres-the-architecture-5dnd</link>
      <guid>https://dev.to/deveshtiw/i-built-a-terminal-ai-assistant-that-never-freezes-heres-the-architecture-5dnd</guid>
      <description>&lt;p&gt;I'm an AI Engineering student, and I got tired of alt-tabbing to a browser &lt;br&gt;
every time I needed to ask an LLM something mid-coding session. So I built &lt;br&gt;
&lt;strong&gt;term-ai&lt;/strong&gt; — a fully async, terminal-native AI assistant that runs as a &lt;br&gt;
real installed CLI tool, not a janky script.&lt;/p&gt;

&lt;p&gt;Here's what I built, why I made specific architectural decisions, and what &lt;br&gt;
broke along the way.&lt;/p&gt;


&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;Type &lt;code&gt;ai-agent&lt;/code&gt; in any terminal. Get a clean, keyboard-driven chat interface &lt;br&gt;
that renders AI responses as live Markdown — code blocks, headings, lists — &lt;br&gt;
right in your shell. Switch between multiple free AI models mid-conversation &lt;br&gt;
without losing context. That's the whole pitch.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Textual&lt;/strong&gt; — for the async TUI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LangChain&lt;/strong&gt; — for conversation memory and orchestration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenRouter API&lt;/strong&gt; — for routing prompts to free LLM models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pyproject.toml&lt;/strong&gt; — packaged as a real CLI tool&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Architecture: Three Layers, One Rule
&lt;/h2&gt;

&lt;p&gt;The one rule: &lt;strong&gt;nothing blocks the UI thread.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Presentation Layer — Textual
&lt;/h3&gt;

&lt;p&gt;Every network call runs inside a Textual &lt;code&gt;@work&lt;/code&gt; worker task. This is the &lt;br&gt;
entire reason the UI never freezes — even on a slow model response, you can &lt;br&gt;
keep scrolling, typing, or switching models while generation runs in the &lt;br&gt;
background.&lt;/p&gt;

&lt;p&gt;Responses render as live Markdown. This matters more than it sounds — &lt;br&gt;
getting back a wall of &lt;code&gt;**bold**&lt;/code&gt; and &lt;code&gt;&lt;/code&gt;&lt;code&gt;code&lt;/code&gt;&lt;code&gt;&lt;/code&gt; asterisks in a &lt;br&gt;
terminal is genuinely painful.&lt;/p&gt;
&lt;h3&gt;
  
  
  Orchestration Layer — LangChain
&lt;/h3&gt;

&lt;p&gt;LangChain manages in-memory conversation state. The key design decision here: &lt;br&gt;
memory is &lt;strong&gt;not tied to a single model&lt;/strong&gt;. You can start a task with one model, &lt;br&gt;
switch to another mid-conversation, and the full context carries over. Same &lt;br&gt;
thread, different brain, zero friction.&lt;/p&gt;
&lt;h3&gt;
  
  
  Routing Layer — OpenRouter
&lt;/h3&gt;

&lt;p&gt;OpenRouter gives you a single API endpoint that routes to 400+ models. For &lt;br&gt;
this project I'm routing exclusively to free-tier models.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Problem That Almost Broke Everything
&lt;/h2&gt;

&lt;p&gt;I had a hardcoded &lt;code&gt;AVAILABLE_MODELS&lt;/code&gt; list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;AVAILABLE_MODELS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gemini 2.5 Pro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google/gemini-2.5-pro:free&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DeepSeek R1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek/deepseek-r1:free&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Llama 4 Maverick&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta-llama/llama-4-maverick:free&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One day, &lt;strong&gt;every single model&lt;/strong&gt; started throwing:&lt;/p&gt;

&lt;p&gt;The entire top tier had quietly gone paid. The &lt;code&gt;:free&lt;/code&gt; suffix in the model &lt;br&gt;
id doesn't mean anything once a provider pulls the model from the free tier &lt;br&gt;
— OpenRouter just updates the pricing fields in their API. The id string &lt;br&gt;
stays the same.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Permanent Fix
&lt;/h3&gt;

&lt;p&gt;I wrote &lt;code&gt;openrouter_models.py&lt;/code&gt; — a self-healing module that fetches &lt;br&gt;
OpenRouter's live model catalog on startup and filters by actual pricing &lt;br&gt;
data, not by the id string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_is_free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pricing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;prompt_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pricing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;completion_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pricing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Pricing is the source of truth — not the :free suffix.
&lt;/span&gt;    &lt;span class="c1"&gt;# A model can keep a :free-looking id after going paid.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prompt_price&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;completion_price&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prompt_price&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;completion_price&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:free&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs once on startup, caches for 6 hours locally, and falls back &lt;br&gt;
gracefully if OpenRouter is unreachable. The &lt;code&gt;AVAILABLE_MODELS&lt;/code&gt; list now &lt;br&gt;
rebuilds itself every time the cache expires — I will never manually patch &lt;br&gt;
a model slug again.&lt;/p&gt;


&lt;h2&gt;
  
  
  Installation (The Actual Modern Way)
&lt;/h2&gt;

&lt;p&gt;This is packaged via &lt;code&gt;pyproject.toml&lt;/code&gt;, not a shell script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/your-username/ai-terminal-app
&lt;span class="nb"&gt;cd &lt;/span&gt;ai-terminal-app
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# add your OpenRouter key&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then from anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ai-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a true global install outside any venv:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Keybindings
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Submit prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+L&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clear memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Quit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mouse&lt;/td&gt;
&lt;td&gt;Scroll, click to navigate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🎙️ &lt;strong&gt;Voice Engine&lt;/strong&gt; — local Whisper STT + TTS, fully offline&lt;/li&gt;
&lt;li&gt;💾 &lt;strong&gt;Persistent Memory&lt;/strong&gt; — SQLite + LangChain so context survives 
between sessions&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: [your repo link here]&lt;/li&gt;
&lt;li&gt;OpenRouter free model catalog: &lt;a href="https://openrouter.ai/collections/free-models" rel="noopener noreferrer"&gt;https://openrouter.ai/collections/free-models&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building terminal tools or working with OpenRouter's free tier, &lt;br&gt;
I'd genuinely like to hear what you're running into — drop it in the &lt;br&gt;
comments.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fysv1z6qa9rylouf5dpuo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fysv1z6qa9rylouf5dpuo.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>ai</category>
      <category>terminal</category>
    </item>
  </channel>
</rss>
