<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Makridenko's blog — English</title>
    <link>https://makridenko.com/?lang=en</link>
    <description>My technical and not-so-technical notes.</description>
    <language>en-us</language>
    <lastBuildDate>Fri, 18 Sep 2026 11:49:33 GMT</lastBuildDate>
    <atom:link href="https://makridenko.com/rss-en.xml" rel="self" type="application/rss+xml"/>
    
    <item>
      <title><![CDATA[Jam 4.0.0]]></title>
      <link>https://makridenko.com/posts/2026/09/16/jam400?lang=en</link>
      <guid>https://makridenko.com/posts/2026/09/16/jam400?lang=en</guid>
      <description><![CDATA[Релиз Jam 4.0.0 что поменялось и куда движемся дальше]]></description>
      <content:encoded><![CDATA[<p><a href="https://github.com/mkrdnk/jam/releases/tag/v4.0.0">Jam 4.0.0</a> is out.</p>
<p>Over the past few months, I&#x27;ve been gradually reworking the core concept of the library. Quite a lot of changes piled up along the way, so 4.0 isn&#x27;t just another set of new features — it&#x27;s a pretty significant rethink of what Jam is supposed to be in the first place.</p>
<p>In short: Jam used to be more of a collection of separate authentication tools, and now it&#x27;s gradually turning into a full-fledged authentication/authorization framework.</p>
<h2>jam.Jam</h2>
<p>The most noticeable change is the completely redesigned <code>jam.Jam</code> facade.</p>
<p>Previously, it was mostly a convenient wrapper around the individual modules:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> jam <span class="hljs-keyword">import</span> Jam

jam = Jam(config=config)

jwt_token = jam.jwt_encode(payload={<span class="hljs-string">&quot;sub&quot;</span>: <span class="hljs-number">1</span>})
payload = jam.jwt_decode(token=jwt_token)
</code></pre>
<p>It worked, but with an API like this, the application still effectively knows that it&#x27;s using JWT.</p>
<p>In 4.0, I wanted to move away from this level of abstraction. The application should work with auth (x/z) concepts, while the actual mechanism — JOSE, PASETO, sessions, etc. — should remain an implementation detail.</p>
<p>So now the main flow looks roughly like this:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> dataclasses <span class="hljs-keyword">import</span> dataclass

<span class="hljs-keyword">from</span> jam <span class="hljs-keyword">import</span> Jam, BaseSubject


<span class="hljs-meta">@dataclass</span>
<span class="hljs-keyword">class</span> <span class="hljs-title class_">User</span>(<span class="hljs-title class_ inherited__">BaseSubject</span>):
    <span class="hljs-built_in">id</span>: <span class="hljs-built_in">int</span>
    email: <span class="hljs-built_in">str</span>
    role: <span class="hljs-built_in">str</span> = <span class="hljs-string">&quot;user&quot;</span>


jam = Jam(config=config)

user = User(
    <span class="hljs-built_in">id</span>=<span class="hljs-number">1</span>,
    email=<span class="hljs-string">&quot;some@mail.com&quot;</span>,
)

<span class="hljs-comment"># Issue credentials for the user.</span>
token = jam.issue(subject=user)

<span class="hljs-comment"># Authenticate the credentials.</span>
principal = jam.authenticate(token=token)

<span class="hljs-keyword">assert</span> principal.subject.<span class="hljs-built_in">id</span> == user.<span class="hljs-built_in">id</span>

<span class="hljs-comment"># And perform authorization.</span>
allowed = jam.authorize(
    principal=principal,
    permission=<span class="hljs-string">&quot;post:delete&quot;</span>,
)
</code></pre>
<p>There are two important entities here: <code>Subject</code> and <code>Principal</code>.</p>
<p><code>Subject</code> is the entity on whose behalf authentication is performed. In the simplest case, that&#x27;s a user, but it doesn&#x27;t necessarily have to be one. It could, for example, be a virtual machine requesting access to certain resources.</p>
<p><code>Principal</code> is the result of authentication. It contains the subject and the information needed for further authorization.</p>
<p>I like this approach much more: the specific authentication mechanism becomes a replaceable implementation detail, while application code works with a single unified model.</p>
<h2>Framework integrations</h2>
<p>Along with that, I&#x27;ve also reworked the framework integrations.</p>
<p>For example, with FastAPI it now looks like this:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> Annotated

<span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> Depends, FastAPI

<span class="hljs-keyword">from</span> jam <span class="hljs-keyword">import</span> Jam
<span class="hljs-keyword">from</span> jam.authz <span class="hljs-keyword">import</span> Principal
<span class="hljs-keyword">from</span> jam.ext.fastapi <span class="hljs-keyword">import</span> JamAuth


jam = Jam(<span class="hljs-string">&quot;config.toml&quot;</span>)
auth = JamAuth(jam)

app = FastAPI()


<span class="hljs-meta">@app.get(<span class="hljs-params"><span class="hljs-string">&quot;/me&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">me</span>(<span class="hljs-params">
    principal: Annotated[
        Principal,
        Depends(<span class="hljs-params">auth</span>), <span class="hljs-comment"># get the principal</span>
    ],
</span>):
    <span class="hljs-keyword">return</span> principal.subject


<span class="hljs-meta">@app.get(<span class="hljs-params"><span class="hljs-string">&quot;/landing&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">landing</span>(<span class="hljs-params">
    principal: Annotated[
        Principal | <span class="hljs-literal">None</span>,
        Depends(<span class="hljs-params">auth.optional</span>), <span class="hljs-comment"># optional authentication</span>
    ],
</span>):
    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">&quot;authenticated&quot;</span>: principal <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>,
    }


<span class="hljs-meta">@app.patch(<span class="hljs-params"><span class="hljs-string">&quot;/posts/{post_id}&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">edit_post</span>(<span class="hljs-params">
    post_id: <span class="hljs-built_in">str</span>,
    principal: Annotated[
        Principal,
        Depends(<span class="hljs-params">auth.require(<span class="hljs-params"><span class="hljs-string">&quot;post:edit&quot;</span></span>)</span>), <span class="hljs-comment"># require a specific permission</span>
    ],
</span>):
    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">&quot;post_id&quot;</span>: post_id,
        <span class="hljs-string">&quot;editor&quot;</span>: principal.subject,
    }
</code></pre>
<p>Application code gets a ready-to-use <code>Principal</code> and doesn&#x27;t have to deal with headers, tokens, or the specific authentication mechanism.</p>
<p>You can simply get the current user:</p>
<pre><code class="hljs language-python">Depends(auth)
</code></pre>
<p>You can allow anonymous access:</p>
<pre><code class="hljs language-python">Depends(auth.optional)
</code></pre>
<p>Or you can require a permission right away:</p>
<pre><code class="hljs language-python">Depends(auth.require(<span class="hljs-string">&quot;post:edit&quot;</span>))
</code></pre>
<p>So authorization can be integrated pretty naturally directly into the application flow.</p>
<h2>The individual mechanisms aren&#x27;t going anywhere</h2>
<p>With all of this, I didn&#x27;t want to turn Jam into a monolith.</p>
<p>The individual modules can still be used independently. If you only need a specific mechanism, you can work with it directly. If you want to build a full authentication flow on top of Jam, that&#x27;s what <code>Jam</code> is now for.</p>
<p>For me, this is a pretty important part of the architecture: modules should remain replaceable and shouldn&#x27;t depend on each other more than necessary.</p>
<h2>Keychain</h2>
<p>But I still consider keychain to be the killer feature of this release.</p>
<p><a href="https://makridenko.com/posts/2026/09/08/keychains-in-authxz-frameworks">I wrote about this idea before</a>, but the mechanism changed a bit during implementation.</p>
<p>The problem I wanted to solve is pretty simple. An application shouldn&#x27;t have to manage the storage and rotation of cryptographic keys itself.</p>
<p>For example, for JWT you can specify a keychain in the configuration:</p>
<pre><code class="hljs language-toml"><span class="hljs-section">[jam.jose.jwt]</span>
<span class="hljs-attr">alg</span> = <span class="hljs-string">&quot;RS256&quot;</span>
<span class="hljs-attr">keychain</span> = <span class="hljs-string">&quot;jose_keys&quot;</span>

<span class="hljs-section">[jam.keychains.jose_keys]</span>
<span class="hljs-comment"># For now, only FileStorage and MemoryStorage are available.</span>
<span class="hljs-comment"># In the future, I want to add HashiCorp Vault,</span>
<span class="hljs-comment"># while other options can be implemented</span>
<span class="hljs-comment"># through the public interface.</span>
<span class="hljs-attr">type</span> = <span class="hljs-string">&quot;FileStorage&quot;</span>
<span class="hljs-attr">path</span> = <span class="hljs-string">&quot;/opt/keys&quot;</span>
</code></pre>
<p>After that, the application works with the keychain:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> jam <span class="hljs-keyword">import</span> Jam
<span class="hljs-keyword">from</span> jam.keychain <span class="hljs-keyword">import</span> FileStorage


jam = Jam(config=<span class="hljs-string">&quot;config.toml&quot;</span>)

keychain: FileStorage = jam.keychains[<span class="hljs-string">&quot;jose_keys&quot;</span>]

<span class="hljs-comment"># Add a key.</span>
<span class="hljs-comment">#</span>
<span class="hljs-comment"># If material isn&#x27;t provided,</span>
<span class="hljs-comment"># Jam will generate the key itself</span>
<span class="hljs-comment"># based on the configuration.</span>
keychain.add(
    key_id=<span class="hljs-string">&quot;my-key-id&quot;</span>,
)

<span class="hljs-comment"># Get the current key.</span>
keychain.current(key_id=<span class="hljs-string">&quot;my-key-id&quot;</span>)

<span class="hljs-comment"># Issue credentials.</span>
token = jam.issue(
    subject={<span class="hljs-string">&quot;sub&quot;</span>: <span class="hljs-number">1</span>},
)

<span class="hljs-comment"># Rotate the key.</span>
keychain.rotate()
</code></pre>
<p>What&#x27;s important here isn&#x27;t so much the ability to call <code>rotate()</code> itself, but the fact that the application doesn&#x27;t have to know where the keys are physically stored or which one is currently in use.</p>
<p>For example, imagine an application has been running in production for several months, and at some point you need to replace the private key. Without a keychain, this can easily turn into a manual procedure: find the key files, replace them, figure out what to do with already issued tokens, and make sure validation of old credentials doesn&#x27;t break. With a keychain, this logic becomes part of the authentication infrastructure. A new key can become the current one, while old keys remain available for validating already issued tokens.</p>
<p>In other words, rotation shouldn&#x27;t automatically mean: <em>old tokens stop working right now.</em></p>
<p>This is especially useful for distributed applications, where multiple service instances may temporarily be running with different versions of the configuration.</p>
<h2>CLI management</h2>
<p>The keychain can also be managed from the command line while the application is running.</p>
<p>For example:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="bash">pip install jamlib[cli]</span>
<span class="hljs-meta prompt_">
$ </span><span class="bash">jam keychain --config jam.toml rotate jose_keys</span>
<span class="hljs-meta prompt_">
$ </span><span class="bash">jam keychain --config jam.toml list jose_keys</span>
</code></pre>
<p>This makes it possible to perform key operations without having to write a separate deployment script or access the storage directly.</p>
<p>For now, there are two storage backends:</p>
<ul>
<li><code>FileStorage</code></li>
<li><code>MemoryStorage</code></li>
</ul>
<p>I want to expand this part further. In particular, I&#x27;m planning to add an integration with HashiCorp Vault.</p>
<p>At the same time, I don&#x27;t want to make a separate implementation for every possible secret storage. For everything else, there will be a public interface that can be used to implement a custom backend.</p>
<h2>What changed architecturally</h2>
<p>Probably the most important change in 4.0.0 is that Jam is gradually becoming more than just a library with a collection of auth tools.</p>
<p>Previously, application code looked roughly like this:</p>
<pre><code class="hljs language-text">Application
    |
    +-- JWT
    +-- PASETO
    +-- Sessions
    +-- OAuth2
    +-- ...
</code></pre>
<p>Now I want to move toward this model:</p>
<pre><code class="hljs language-text">Application
    |
    +-- Jam
          |
          +-- Authentication
          |
          +-- Authorization
          |
          +-- JWT
          +-- PASETO
          +-- Sessions
          +-- OAuth2
          +-- ...
</code></pre>
<p>The specific mechanisms still remain independent modules that can be used on their own. So you get a certain abstraction layer over authentication, but without trying to hide absolutely everything behind one huge class.</p>
<h2>What&#x27;s next?</h2>
<p>In the near-term plans:</p>
<ul>
<li>extending keychain support to other modules</li>
<li>HashiCorp storage</li>
<li>SAML support in the main <code>jam.Jam</code> facade</li>
<li>Macaroon + Biscuit tokens</li>
<li>expanding authz configuration capabilities</li>
</ul>
<p><a href="https://github.com/mkrdnk/jam/releases/tag/v4.0.0">GitHub Release</a> | <a href="https://jam.makridenko.ru">Documentation</a></p>]]></content:encoded>
      <pubDate>Wed, 16 Sep 2026 00:00:00 GMT</pubDate>
      <author>undefined</author>
      <category>Python</category>
      <category>Devlog</category>
    </item>
    <item>
      <title><![CDATA[Building an Internal LLM: vLLM, OpenWebUI, and a Few Hacks]]></title>
      <link>https://makridenko.com/posts/2026/09/01/iternal-llm?lang=en</link>
      <guid>https://makridenko.com/posts/2026/09/01/iternal-llm?lang=en</guid>
      <description><![CDATA[How I deployed an internal LLM on an H100, wrestled with MIG, caching, context windows, and gradually turned a personal experiment into a company-wide service.]]></description>
      <content:encoded><![CDATA[<p>It so happened that the company I currently work for wasn&#x27;t just unfamiliar with AI agents before I joined — most developers had never even seen what all this AI hype was about. Classic enterprise environment.
As a result, I ended up becoming both the initiator and the driving force behind introducing AI into the development process.</p>
<blockquote>
<p>So I decided to share my experience and show exactly what I did, so you can reproduce it yourself if needed.</p>
</blockquote>
<p>Since this was still a heavily regulated enterprise environment, the question of running an LLM locally, inside the company perimeter, came up almost immediately. Claude, Codex, and other cloud-based solutions were completely off-limits.</p>
<hr/>
<h1>What I Had</h1>
<p>After several months of negotiations, I was finally given a machine with an NVIDIA H100, and I started figuring out how all of this worked. My previous experience was limited to running models on a home PC, so at first it seemed like everything would be more or less the same.
It didn&#x27;t take long to discover that enterprise infrastructure always has a few surprises waiting for you.
My GPU only had 80 GB of VRAM. By modern model standards, that&#x27;s not a lot, so model selection had to be done carefully.</p>
<p>I considered:</p>
<ul>
<li>DeepSeek</li>
<li>Qwen3</li>
<li>Qwen3.6</li>
<li>Qwen3-Coder</li>
</ul>
<p>After a few tests, I settled on <code>Qwen/Qwen3-Coder-30B-A3B-Instruct</code> because <code>deepseek-ai/deepseek-coder-33b-base</code> (the only reasonably usable DeepSeek model that fit into memory) turned out to be fairly weak, while <code>Qwen3.6</code> wouldn&#x27;t start at all because of driver issues.</p>
<p>More on that in a moment.</p>
<hr/>
<h1>First vLLM Launch</h1>
<p>Like any reasonable person, I started by deploying the model with vLLM and calling it directly through the API.</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">mkdir</span> vllm &amp;&amp; <span class="hljs-built_in">cd</span> vllm

pip install --upgrade pip
pip install uv

uv venv --python 3.12 --seed --managed-python
<span class="hljs-built_in">source</span> ~/vllm/.venv/bin/activate

uv pip install -U \
  <span class="hljs-string">&quot;transformers&lt;5&quot;</span> \
  <span class="hljs-string">&quot;vllm==0.10.2&quot;</span>

uv run vllm serve Qwen/Qwen3-Coder-30B-A3B-Instruct \
  --host 0.0.0.0 \
  --port 8000
</code></pre>
<p>That&#x27;s where the adventure began.</p>
<h1>When Enterprise Infrastructure Meets LLMs</h1>
<p>Some of you may have noticed that the versions of <code>vllm</code> and <code>transformers</code> are far from the latest. That wasn&#x27;t accidental.
The GPU was provisioned through MIG, and along with it came a whole collection of limitations. The biggest issue was the drivers. I couldn&#x27;t update them, and due to a combination of architectural constraints and bureaucracy, the administrators couldn&#x27;t update them either.
That&#x27;s exactly why I never managed to get Qwen 3.6 running.</p>
<p>But the fun didn&#x27;t stop there.
Every night at around 2:00 AM, the MIG instance would disappear from the system for a few seconds and then come back with a new UID. I had no interest in figuring out who was responsible, so I chose the most engineering-oriented solution possible:
Every morning at 5:00 AM, the server simply reboots.
As a result:</p>
<ul>
<li>a fresh UID appears;</li>
<li>accumulated issues disappear;</li>
<li>the system gets a preventive reboot before the workday starts.</li>
</ul>
<p>Yes, it&#x27;s a hack, but it works.</p>
<p><img src="https://makridenko.com/imgs/llm/flow-1.svg" alt="flow-1"/></p>
<hr/>
<h1>Why vLLM Alone Isn&#x27;t Enough</h1>
<p>For several days I used the model on my own without any issues.
It quickly became obvious, however, that this approach only works for a single person. If other employees started using the model, I would need:</p>
<ul>
<li>a web interface;</li>
<li>user management;</li>
<li>integration with corporate authentication;</li>
<li>the ability to revoke access quickly;</li>
<li>an open-source solution that could be modified internally.</li>
</ul>
<p>After a bit of research, I settled on OpenWebUI. It had everything I needed and then some.</p>
<h1>OpenWebUI as the Entry Point</h1>
<p>Initially, OpenWebUI lived on the same machine as vLLM.
Very quickly, though, it became obvious that it was consuming resources I&#x27;d rather leave available for the model itself. So I deployed a separate virtual machine.</p>
<p>The resulting architecture looked like this:
The user interacts with OpenWebUI, and OpenWebUI sends requests to vLLM.</p>
<p>This turned out to be convenient for several reasons:</p>
<ul>
<li>access can be restricted to specific users or groups;</li>
<li>usage statistics can be collected;</li>
<li>response ratings can be gathered;</li>
<li>system prompts can be managed centrally.</li>
</ul>
<p><img src="https://makridenko.com/imgs/llm/flow-2.svg" alt="flow-2"/></p>
<hr/>
<h1>Squeezing the Most Out of 80 GB of Memory</h1>
<p>At this point I started experimenting with model settings.
My first idea was to increase the context window.
I tried this:</p>
<pre><code class="hljs language-bash">uv run vllm serve Qwen/Qwen3-Coder-30B-A3B-Instruct \
  --max-model-len 120000
</code></pre>
<p>It didn&#x27;t work.</p>
<p>After a series of experiments, I arrived at the following configuration:</p>
<pre><code class="hljs language-bash">uv run vllm serve Qwen/Qwen3-Coder-30B-A3B-Instruct \
  --host 0.0.0.0 \
  --port 8000 \
  --dtype bfloat16 \
  --max-model-len 70000 \
  --gpu-memory-utilization 0.82 \
  --max-num-seqs 8 \
  --max-num-batched-tokens 8192 \
  --enable-auto-tool-choice \
  --tool-call-parser qwen3_coder \
  --served-model-name Qwen-Coder \
  --enable-prefix-caching \
  --enable-chunked-prefill \
  --kv-cache-dtype fp8
</code></pre>
<p>This gave me roughly 70,000 tokens of context.
Unfortunately, it didn&#x27;t take long to discover that even that wasn&#x27;t enough.</p>
<hr/>
<h1>Hack #1: Rebuilding the Cache After Reboot</h1>
<p>The most obvious solution was to use <strong>prefix caching</strong>. The problem was that every morning the server rebooted, and the entire cache disappeared along with it.</p>
<p>So another hack was born.
After startup, the system automatically sends a predefined set of popular requests to the model.</p>
<pre><code class="hljs language-python"><span class="hljs-comment"># -*- coding: utf-8 -*-</span>
<span class="hljs-comment"># /opt/openwebui/scripts/token_recache_service.py</span>

RECACHE_PROMPT: <span class="hljs-built_in">str</span> = load_recache_data()

client.chat.completions.create(
    model=<span class="hljs-string">&quot;qwen&quot;</span>,
    messages=[
        {<span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;system&quot;</span>, <span class="hljs-string">&quot;content&quot;</span>: RECACHE_PROMPT},
        {<span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;user&quot;</span>, <span class="hljs-string">&quot;content&quot;</span>: <span class="hljs-string">&quot;warmup&quot;</span>}
    ]
)
</code></pre>
<p>In practice, I&#x27;m simply forcing the model to recompute the tokens I want cached.
The list of popular requests was assembled together with an AI agent and continues to grow over time.</p>
<p>So far, this solution has been sufficient.</p>
<p><img src="https://makridenko.com/imgs/llm/flow-3.svg" alt="flow-3"/></p>
<hr/>
<h1>Wrapping Everything Into a Service</h1>
<p>From there, it was mostly standard infrastructure work.</p>
<p>I deployed <strong>PostgreSQL</strong> instead of <strong>SQLite</strong>, configured <strong>nginx</strong>, issued certificates, and locked down access to the <strong>vLLM</strong> server as much as possible.</p>
<p>Only <strong>OpenWebUI</strong> is allowed to communicate with the model.</p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">postgres:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:17</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">open-webui-postgres</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>

  <span class="hljs-attr">open-webui:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">ghcr.io/open-webui/open-webui:main</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">open-webui</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
</code></pre>
<p>This immediately solved several problems.</p>
<ul>
<li>First, nobody can access the model directly anymore.</li>
<li>Second, if I need to perform maintenance or testing, I can simply disable the model in OpenWebUI.</li>
<li>Third, the system is no longer limited to developers using VSCode, Zed, or Opencode. Any employee can use it through a browser.</li>
</ul>
<p><img src="https://makridenko.com/imgs/llm/schema-2.svg" alt="scheme-2"/></p>
<hr/>
<h1>Hack #2: The Model Wants Coffee Too</h1>
<p>After some time, I noticed an interesting pattern.
If nobody used the model for a long period, the first few requests in the morning performed noticeably worse. Responses took longer, hallucinations became more frequent, and overall behavior felt strange.</p>
<p>After several requests, everything returned to normal.
At first I assumed the GPU was simply sitting idle and &quot;cooling down.&quot;
I did some searching, found that similar observations weren&#x27;t unique to me, and wrote a simple script that generated a small amount of continuous GPU load.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">import</span> time
<span class="hljs-keyword">import</span> torch

DEVICE = <span class="hljs-string">&quot;cuda&quot;</span>

a = torch.randn((<span class="hljs-number">2048</span>, <span class="hljs-number">2048</span>), device=DEVICE, dtype=torch.float16)
b = torch.randn((<span class="hljs-number">2048</span>, <span class="hljs-number">2048</span>), device=DEVICE, dtype=torch.float16)

<span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
    c = torch.matmul(a, b)
    torch.cuda.synchronize()
    time.sleep(<span class="hljs-number">10</span>)
</code></pre>
<p>It worked. But it felt like I was wasting resources.
Later I replaced it with a different approach.
Now, every few minutes, the system sends a meaningless request to the model:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">import</span> requests

URL = os.getenv(<span class="hljs-string">&quot;LLM_API&quot;</span>)
TIMEOUT = <span class="hljs-number">300</span>

<span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
    <span class="hljs-keyword">try</span>:
        requests.post(
            URL,
            json={
                <span class="hljs-string">&quot;model&quot;</span>: <span class="hljs-string">&quot;qwen&quot;</span>,
                <span class="hljs-string">&quot;messages&quot;</span>: [
                    {<span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;user&quot;</span>, <span class="hljs-string">&quot;content&quot;</span>: <span class="hljs-string">&quot;ping&quot;</span>}
                ],
                <span class="hljs-string">&quot;max_tokens&quot;</span>: <span class="hljs-number">1</span>,
            },
            timeout=<span class="hljs-number">30</span>,
        )
    <span class="hljs-keyword">except</span> Exception:
        <span class="hljs-keyword">pass</span>

    time.sleep(TIMEOUT)
</code></pre>
<p>As a result, the model never stays idle for too long, and the first real user requests tend to perform much more consistently.</p>
<p><img src="https://makridenko.com/imgs/llm/flow-4.svg" alt="flow-4"/></p>
<hr/>
<h1>Final Architecture</h1>
<p>As a finishing touch, our DevOps engineer and I configured authentication through FreeIPA, issued an internal certificate, and added the service to the corporate DNS.</p>
<p>The final architecture ended up looking like this:</p>
<p><img src="https://makridenko.com/imgs/llm/scheme.svg" alt="scheme"/></p>
<hr/>
<h1>What&#x27;s Next?</h1>
<p>The system is now being actively used within the company and is gradually being enhanced with additional features. As a next step, I want to move the project knowledge into a separate RAG service and focus separately on the long-term storage and restoration of caches. And very soon I’ll have a machine running on an H200, where I’ll be testing more sophisticated models.</p>
<p>But that’s a story for the next article.</p>]]></content:encoded>
      <pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate>
      <author>Adrian Makridenko</author>
      <enclosure url="https://makridenko.com/imgs/llm/flow-1.svg" type="image/svg+xml" length="0"/>
      <category>AI</category>
      <category>Tutorial</category>
    </item>
    <item>
      <title><![CDATA[Delta by Zed]]></title>
      <link>https://makridenko.com/posts/2026/08/30/delta-by-zed?lang=en</link>
      <guid>https://makridenko.com/posts/2026/08/30/delta-by-zed?lang=en</guid>
      <description><![CDATA[I got into the Delta whitelist and spent some time using it for work and not-so-work-related stuff. And I really liked it — this is exactly what I was missing. It’s not some ugly terminal thing written in JS and React, nor a janky stripped-down extension for Zed or VS Code. It’s a fast, native app that does a lot right out of the box.]]></description>
      <content:encoded><![CDATA[<p><img src="https://makridenko.com/imgs/delta-by-zed/delta-invite-mail.png" alt="delta-invite-mail"/></p>
<p>I got into the <a href="https://delta.dev">Delta</a> whitelist and spent some time using it for work and not-so-work-related stuff. And I really liked it — this is exactly what I was missing. It’s not some ugly terminal thing written in JS and React, nor a janky stripped-down extension for Zed or VS Code. It’s a <strong>native</strong>, fast app that does a lot right out of the box.</p>
<p><img src="https://makridenko.com/imgs/delta-by-zed/delta-ui.png" alt="ui"/></p>
<h2>What does it have that Claude Code, Codex, and OpenCode don’t?</h2>
<h3>First of all, really damn good code review</h3>
<p><img src="https://makridenko.com/imgs/delta-by-zed/delta-review.png" alt="review"/></p>
<p>Before this, I used <a href="https://revdiff.com">revdiff</a> by <a href="https://github.com/umputun">Umputun</a>, but objectively speaking, it’s a pretty hacky workaround. Opening some TUI program through tmux and terminal overlays, only for it to write something like <code>line_number: comment</code> when you exit — eh, not exactly great.</p>
<p>Here, you just walk through the code, leave comments, and then send them to the agent in one action. It’s very similar to how we do reviews in GitLab or GitHub. Really damn nice.</p>
<h3>Second, worktree-based workflow</h3>
<p>The folks at <a href="https://atom-editor.cc/">Atom</a> — the same people building Zed and Delta — have been pushing Git worktrees for a long time, going all the way back to the Atom days.</p>
<p>I like this approach too. Especially in the age of agentic coding, I don’t really want to let an agent loose directly in my working tree. It’s much more convenient to create a worktree from the current state of the project and let the agent have fun there. After the review, the changes can be moved into the main branch.</p>
<p>That’s exactly what <strong>Delta</strong> does: each thread gets its own worktree, and all the work happens there. It’s both safer and more convenient, especially when multiple agents are working in parallel.</p>
<p><img src="https://makridenko.com/imgs/delta-by-zed/delta-tree.png" alt="tree-pic"/></p>
<h3>Third, a rather niche use case, but...</h3>
<p>Collaborative work.</p>
<p>I can share a session and work on a task together with a colleague in real time. I <strong>really</strong> like this feature in Zed itself. At work, I occasionally use it to look at code together with colleagues, and it’s much more convenient than sharing your screen during a call and trying to show someone what you’re doing.</p>
<p>And once coding started turning into chasing AI agents around, it became logical to share not the code itself, but the session: the plans, the reasoning, the same code, and now also the diffs produced by the agent.</p>
<p>Not many people need this, but personally, I consider it one of the best features in both Zed and Delta.</p>
<h3>Fourth, NATIVENESS</h3>
<p>YES. GOD. YES.</p>
<p>I am so damn tired of this entire zoo of Electron, JS, and TS. Why? What for?</p>
<p>(Ironically, the people who created Zed also created Electron for Atom back in the day, and now they’re trying to do everything natively. Making up for their sins 100%.)</p>
<p>Why, for example, would you write Bitwarden in TypeScript? Both the desktop app and the CLI. Why would you write Claude Code in TypeScript? And React on top of that?</p>
<p>Electron has generally become a genuine plague of modern desktop software, so the fact that Delta is fast and native is a <strong>huge</strong> plus.</p>
<hr/>
<h3>What is it actually missing?</h3>
<h4>First and foremost, custom providers.</h4>
<p>I really want to connect a work model using our own tokens (mostly so the security people don’t come after me) and work through it. Maybe this isn’t available yet simply because Delta is still in closed beta. But I didn’t see a single word about custom providers on the roadmap.</p>
<h4>Then, of course, Git integration.</h4>
<p>Right now, they suggest moving changes over using <code>stash</code> or checking out commits from the worktree in your terminal outside of Delta. I’m not really against that, but I’d like to have a slightly more convenient way of doing it. Fortunately, this is already on the roadmap, so they’ll probably improve it.</p>
<h4>And finally, switching agents.</h4>
<p>I’d like to first build a plan with a read-only agent and then hand it over to another agent to actually do the work. That’s not possible yet, but we’ll see how the product evolves.</p>
<hr/>
<p>At some point while working, I caught myself thinking that I don’t really perceive Delta as yet another tool for working with <strong>AI</strong> — or even as an “agentic IDE.”</p>
<p>With the arrival of full-fledged agents in our workflow, the development process itself has changed. Previously, a code editor was primarily where you wrote code yourself. Now, you open it mostly to read the code the agent wrote — and even that happens pretty rarely, because the diffs in Claude Code / Codex are often enough.</p>
<p>In Delta, all of this is there. You can read the entire project, manually edit the code if you need to, and leave comments for the robot even outside of diffs.</p>
<p>That’s exactly why Delta feels less like yet another little chat window bolted onto an editor and more like an attempt to build the next generation of code editor.</p>
<p>Here, the agent isn’t an add-on to the familiar workflow anymore. It becomes one of the core entities of the system, alongside the code (or perhaps even above it), Git, and the developer.</p>]]></content:encoded>
      <pubDate>Sun, 30 Aug 2026 00:00:00 GMT</pubDate>
      <author>Adrian Makridenko</author>
      <enclosure url="https://makridenko.com/imgs/delta-by-zed/delta-ui.png" type="image/png" length="0"/>
      <category>AI</category>
      <category>Tools</category>
    </item>
  </channel>
</rss>